<repo_name>bwaldvogel/mongo-java-server<file_sep>/h2-backend/src/main/java/de/bwaldvogel/mongo/H2MongoServer.java
package de.bwaldvogel.mongo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.bwaldvogel.mongo.backend.h2.H2Backend;

public class H2MongoServer extends MongoServer {

    private static final Logger log = LoggerFactory.getLogger(H2MongoServer.class);

    public static void main(String[] args) {
        final MongoServer mongoServer;
        if (args.length == 1) {
            String fileName = args[0];
            mongoServer = new H2MongoServer(fileName);
        } else {
            mongoServer = new H2MongoServer();
        }

        mongoServer.bind("localhost", 27017);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            log.info("shutting down {}", mongoServer);
            mongoServer.shutdownNow();
        }));
    }

    public H2MongoServer() {
        super(H2Backend.inMemory());
    }

    public H2MongoServer(String fileName) {
        super(new H2Backend(fileName));
    }
}
<file_sep>/postgresql-backend/src/main/java/de/bwaldvogel/mongo/backend/postgresql/JsonConverter.java
package de.bwaldvogel.mongo.backend.postgresql;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

import de.bwaldvogel.mongo.bson.BinData;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.bson.LegacyUUID;
import de.bwaldvogel.mongo.bson.ObjectId;
import de.bwaldvogel.mongo.exception.MongoServerException;

final class JsonConverter {

    private static final ObjectMapper objectMapper = objectMapper();

    private JsonConverter() {
    }

    private static ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);

        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), DefaultTyping.JAVA_LANG_OBJECT, JsonTypeInfo.As.PROPERTY);

        objectMapper.addMixIn(LegacyUUID.class, LegacyUUIDJsonMixIn.class);
        objectMapper.addMixIn(BinData.class, BinDataJsonMixIn.class);

        objectMapper.registerSubtypes(ObjectId.class);
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
        objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

        SimpleModule module = new SimpleModule();
        module.addAbstractTypeMapping(Set.class, LinkedHashSet.class);
        module.addAbstractTypeMapping(Map.class, LinkedHashMap.class);
        objectMapper.registerModule(module);

        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        objectMapper.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true);
        objectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, true);
        return objectMapper;
    }

    static String toJson(Object object) {
        ObjectWriter writer = objectMapper.writer();
        try {
            return writer.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            throw new MongoServerException("Failed to serialize value to JSON", e);
        }
    }

    static Document fromJson(String json) throws IOException {
        ObjectReader reader = objectMapper.reader();
        return reader.forType(Document.class).readValue(json);
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/NumericUtils.java
package de.bwaldvogel.mongo.backend;

import java.math.BigDecimal;

import de.bwaldvogel.mongo.bson.Decimal128;

public class NumericUtils {

    @FunctionalInterface
    public interface BigDecimalCalculation {
        BigDecimal apply(BigDecimal a, BigDecimal b);
    }

    @FunctionalInterface
    public interface DoubleCalculation {
        double apply(double a, double b);
    }

    @FunctionalInterface
    public interface LongCalculation {
        long apply(long a, long b);
    }

    private static Number calculate(Number a, Number b,
                                    LongCalculation longCalculation,
                                    DoubleCalculation doubleCalculation,
                                    BigDecimalCalculation bigDecimalCalculation) {
        if (a instanceof Decimal128 || b instanceof Decimal128) {
            BigDecimal result = bigDecimalCalculation.apply(toBigDecimal(a), toBigDecimal(b));
            return new Decimal128(result);
        } else if (a instanceof Double || b instanceof Double) {
            return Double.valueOf(doubleCalculation.apply(a.doubleValue(), b.doubleValue()));
        } else if (a instanceof Float || b instanceof Float) {
            double result = doubleCalculation.apply(a.doubleValue(), b.doubleValue());
            return Float.valueOf((float) result);
        } else if (a instanceof Long || b instanceof Long) {
            return Long.valueOf(longCalculation.apply(a.longValue(), b.longValue()));
        } else if (a instanceof Integer || b instanceof Integer) {
            long result = longCalculation.apply(a.longValue(), b.longValue());
            int intResult = (int) result;
            if (intResult == result) {
                return Integer.valueOf(intResult);
            } else {
                return Long.valueOf(result);
            }
        } else if (a instanceof Short || b instanceof Short) {
            long result = longCalculation.apply(a.longValue(), b.longValue());
            short shortResult = (short) result;
            if (shortResult == result) {
                return Short.valueOf(shortResult);
            } else {
                return Long.valueOf(result);
            }
        } else {
            throw new UnsupportedOperationException("cannot calculate on " + a + " and " + b);
        }
    }

    private static BigDecimal toBigDecimal(Number value) {
        return Decimal128.fromNumber(value).toBigDecimal();
    }

    public static Number addNumbers(Number one, Number other) {
        return calculate(one, other, Long::sum, Double::sum, BigDecimal::add);
    }

    public static Number subtractNumbers(Number one, Number other) {
        return calculate(one, other, (a, b) -> a - b, (a, b) -> a - b, BigDecimal::subtract);
    }

    public static Number multiplyNumbers(Number one, Number other) {
        return calculate(one, other, (a, b) -> a * b, (a, b) -> a * b, BigDecimal::multiply);
    }

}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/aggregation/stage/UnsetStageTest.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import static de.bwaldvogel.mongo.TestUtils.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;

import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.MongoServerError;

class UnsetStageTest {

    @Test
    void testUnset() throws Exception {
        assertThat(unset("field1",
            json("_id: 1, field1: 'value1'"),
            json("_id: 2, field1: 'value1', field2: 'value2'"),
            json("_id: 3, field2: 'value2'"),
            json("_id: 4")))
            .containsExactly(
                json("_id: 1"),
                json("_id: 2, field2: 'value2'"),
                json("_id: 3, field2: 'value2'"),
                json("_id: 4")
            );
    }

    @Test
    void testUnsetMultipleFields() throws Exception {
        assertThat(unset(List.of("field1", "field2"),
            json("_id: 1, field1: 'value1'"),
            json("_id: 2, field1: 'value1', field2: 'value2'"),
            json("_id: 3, field2: 'value2', field3: 'value3'"),
            json("_id: 4, field3: 'value3'"),
            json("_id: 5")))
            .containsExactly(
                json("_id: 1"),
                json("_id: 2"),
                json("_id: 3, field3: 'value3'"),
                json("_id: 4, field3: 'value3'"),
                json("_id: 5")
            );
    }

    @Test
    void testUnsetWithSubdocument() throws Exception {
        assertThat(unset("fields.field1",
            json("_id: 1, fields: { field1: 'value1' }"),
            json("_id: 2, fields: { field1: 'value1', field2: 'value2' }"),
            json("_id: 3, fields: { field2: 'value2' }"),
            json("_id: 4, fields: { }"),
            json("_id: 5")))
            .containsExactly(
                json("_id: 1, fields: { }"),
                json("_id: 2, fields: { field2: 'value2' }"),
                json("_id: 3, fields: { field2: 'value2' }"),
                json("_id: 4, fields: { }"),
                json("_id: 5")
            );
    }

    @Test
    void testIllegalUnset() throws Exception {
        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> new UnsetStage(""))
            .withMessage("[Error 40352] Invalid $unset :: caused by :: FieldPath cannot be constructed with empty string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> new UnsetStage(List.of(123, 456)))
            .withMessage("[Error 31120] $unset specification must be a string or an array containing only string values");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> new UnsetStage(123))
            .withMessage("[Error 31120] $unset specification must be a string or an array containing only string values");
    }

    private List<Document> unset(Object input, Document... documents) {
        return new UnsetStage(input).apply(Stream.of(documents)).collect(Collectors.toList());
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/stage/GraphLookupStage.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

import de.bwaldvogel.mongo.MongoCollection;
import de.bwaldvogel.mongo.MongoDatabase;
import de.bwaldvogel.mongo.backend.Utils;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.FailedToParseException;

public class GraphLookupStage extends AbstractLookupStage {

    private static final String START_WITH = "startWith";
    private static final String CONNECT_FROM_FIELD = "connectFromField";
    private static final String CONNECT_TO_FIELD = "connectToField";
    private static final String MAX_DEPTH = "maxDepth";
    private static final String DEPTH_FIELD = "depthField";

    private static final Set<String> CONFIGURATION_KEYS;
    static {
        CONFIGURATION_KEYS = new HashSet<>();
        CONFIGURATION_KEYS.add(FROM);
        CONFIGURATION_KEYS.add(START_WITH);
        CONFIGURATION_KEYS.add(CONNECT_FROM_FIELD);
        CONFIGURATION_KEYS.add(CONNECT_TO_FIELD);
        CONFIGURATION_KEYS.add(AS);
        CONFIGURATION_KEYS.add(MAX_DEPTH);
        CONFIGURATION_KEYS.add(DEPTH_FIELD);
    }

    private final String connectFromField;
    private final String connectToField;
    private final String asField;
    private final Integer maxDepth;
    private final String depthField;

    private final MongoCollection<?> collection;

    public GraphLookupStage(Document configuration, MongoDatabase mongoDatabase) {
        String from = readStringConfigurationProperty(configuration, FROM);
        collection = mongoDatabase.resolveCollection(from, false);
        readStringConfigurationProperty(configuration, FROM);
        readVariableConfigurationProperty(configuration, START_WITH);
        connectFromField = readStringConfigurationProperty(configuration, CONNECT_FROM_FIELD);
        connectToField = readStringConfigurationProperty(configuration, CONNECT_TO_FIELD);
        asField = readStringConfigurationProperty(configuration, AS);
        maxDepth = readOptionalIntegerConfigurationProperty(configuration, MAX_DEPTH);
        depthField = readOptionalStringConfigurationProperty(configuration, DEPTH_FIELD);
        ensureAllConfigurationPropertiesAreKnown(configuration, CONFIGURATION_KEYS);
    }

    @Override
    public String name() {
        return "$graphLookup";
    }

    Integer readOptionalIntegerConfigurationProperty(Document configuration, String name) {
        Object value = configuration.get(name);
        if (value == null) {
            return null;
        }
        if (value instanceof Integer) {
            return (Integer) value;
        }
        throw new FailedToParseException("'" + name + "' option to \" + stageName + \" must be a integer, but was type " + Utils.describeType(value));
    }

    String readOptionalStringConfigurationProperty(Document configuration, String name) {
        Object value = configuration.get(name);
        if (value == null) {
            return null;
        }
        if (value instanceof String) {
            return (String) value;
        }
        throw new FailedToParseException("'" + name + "' option to \" + stageName + \" must be a string, but was type " + Utils.describeType(value));
    }

    void readVariableConfigurationProperty(Document configuration, String name) {
        Object value = configuration.get(name);
        if (value == null) {
            throw new FailedToParseException("missing '" + name + "' option to $graphLookup stage specification: " + configuration);
        }
        if (value instanceof String) {
            return;
        }
        throw new FailedToParseException("'" + name + "' option to $graphLookup must be a string, but was type " + Utils.describeType(value));
    }

    @Override
    public Stream<Document> apply(Stream<Document> stream) {
        return stream.map(this::connectRemoteDocument);
    }

    private Document connectRemoteDocument(Document document) {
        Object value = document.get(connectFromField);
        List<Document> documentList = new ArrayList<>();
        findLinkedDocuments(0, documentList, value);
        Document result = document.clone();
        result.put(asField, documentList);
        return result;
    }

    private List<Document> findLinkedDocuments(long depth, final List<Document> linked, Object value) {

        if (maxDepth != null && depth > maxDepth) {
            return linked;
        }

        if (value instanceof List) {
            return ((List<?>) value).stream()
                .flatMap(item -> findLinkedDocuments(depth + 1, linked, item).stream())
                .collect(toList());
        }
        Document query = new Document(connectToField, value);

        List<Document> newlyLinkedDocuments = collection.handleQueryAsStream(query).collect(toList());
        for (Document newDocument : newlyLinkedDocuments) {
            Object newValue = newDocument.get(connectFromField);
            if (depthField != null) {
                newDocument.put(depthField, depth);
            }
            linked.add(0, newDocument);
            findLinkedDocuments(depth + 1, linked, newValue);
        }
        return linked;
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/IndexBuildFailedException.java
package de.bwaldvogel.mongo.exception;

import java.util.UUID;

import de.bwaldvogel.mongo.MongoCollection;

public class IndexBuildFailedException extends MongoServerError {
    private static final long serialVersionUID = 1L;

    public IndexBuildFailedException(MongoServerError e, MongoCollection<?> collection) {
        super(e.getCode(), e.getCodeName(), "Index build failed: " + UUID.randomUUID() + ": " +
            "Collection " + collection.getFullName() + " ( " + collection.getUuid() + " ) :: caused by :: "
            + e.getMessageWithoutErrorCode(), e);

    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/stage/TerminalStage.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import java.util.stream.Stream;

import de.bwaldvogel.mongo.bson.Document;

public abstract class TerminalStage implements AggregationStage {

    abstract void applyLast(Stream<Document> stream);

    @Override
    public final Stream<Document> apply(Stream<Document> stream) {
        applyLast(stream);
        return Stream.empty();
    }
}
<file_sep>/memory-backend/src/main/java/de/bwaldvogel/mongo/backend/memory/MemoryCollection.java
package de.bwaldvogel.mongo.backend.memory;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import de.bwaldvogel.mongo.MongoDatabase;
import de.bwaldvogel.mongo.backend.AbstractSynchronizedMongoCollection;
import de.bwaldvogel.mongo.backend.CollectionOptions;
import de.bwaldvogel.mongo.backend.CursorRegistry;
import de.bwaldvogel.mongo.backend.DocumentWithPosition;
import de.bwaldvogel.mongo.backend.QueryResult;
import de.bwaldvogel.mongo.bson.Document;

public class MemoryCollection extends AbstractSynchronizedMongoCollection<Integer> {

    private final List<Document> documents = new ArrayList<>();
    private final Queue<Integer> emptyPositions = new LinkedList<>();
    private final AtomicInteger dataSize = new AtomicInteger();

    public MemoryCollection(MongoDatabase database, String collectionName,
                            CollectionOptions options, CursorRegistry cursorRegistry) {
        super(database, collectionName, options, cursorRegistry);
    }

    @Override
    protected void updateDataSize(int sizeDelta) {
        dataSize.addAndGet(sizeDelta);
    }

    @Override
    protected int getDataSize() {
        return dataSize.get();
    }

    @Override
    protected Integer addDocumentInternal(Document document) {
        Integer position = emptyPositions.poll();
        if (position == null) {
            position = Integer.valueOf(documents.size());
        }

        if (position.intValue() == documents.size()) {
            documents.add(document);
        } else {
            documents.set(position.intValue(), document);
        }
        return position;
    }

    @Override
    protected QueryResult matchDocuments(Document query, Document orderBy, int numberToSkip, int limit, int batchSize, Document fieldSelector) {
        Iterable<Document> documents = iterateAllDocuments(orderBy);
        Stream<Document> documentStream = StreamSupport.stream(documents.spliterator(), false);
        return matchDocumentsFromStream(documentStream, query, orderBy, numberToSkip, limit, batchSize, fieldSelector);
    }

    private Iterable<Document> iterateAllDocuments(Document orderBy) {
        DocumentIterable documentIterable = new DocumentIterable(this.documents);
        if (isNaturalDescending(orderBy)) {
            return documentIterable.reversed();
        } else {
            return documentIterable;
        }
    }

    @Override
    public synchronized int count() {
        return documents.size() - emptyPositions.size();
    }

    @Override
    public synchronized boolean isEmpty() {
        return documents.isEmpty() || super.isEmpty();
    }

    @Override
    protected Integer findDocumentPosition(Document document) {
        int position = documents.indexOf(document);
        if (position < 0) {
            return null;
        }
        return Integer.valueOf(position);
    }

    @Override
    protected Stream<DocumentWithPosition<Integer>> streamAllDocumentsWithPosition() {
        return IntStream.range(0, documents.size())
            .filter(position -> !emptyPositions.contains(position))
            .mapToObj(index -> new DocumentWithPosition<>(documents.get(index), index));
    }

    @Override
    protected void removeDocument(Integer position) {
        documents.set(position.intValue(), null);
        emptyPositions.add(position);
    }

    @Override
    protected Document getDocument(Integer position) {
        return documents.get(position.intValue());
    }

    @Override
    protected void handleUpdate(Integer position, Document oldDocument, Document newDocument) {
        // noop
    }

}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/MissingTest.java
package de.bwaldvogel.mongo.backend;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

public class MissingTest {

    @Test
    void testIsNullOrMissing() throws Exception {
        assertThat(Missing.isNullOrMissing(null)).isTrue();
        assertThat(Missing.isNullOrMissing(Missing.getInstance())).isTrue();
        assertThat(Missing.isNullOrMissing("value")).isFalse();
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/bson/BsonTimestamp.java
package de.bwaldvogel.mongo.bson;

import java.time.Instant;
import java.util.Objects;

public final class BsonTimestamp implements Bson, Comparable<BsonTimestamp> {

    private static final long serialVersionUID = 1L;

    private final long value;

    protected BsonTimestamp() {
        this(0);
    }

    public BsonTimestamp(long value) {
        this.value = value;
    }

    public BsonTimestamp(Instant instant, int increment) {
        value = (instant.getEpochSecond() << 32) | (increment & 0xFFFFFFFFL);
    }

    public long getValue() {
        return value;
    }

    public int getTime() {
        return (int) (value >> 32);
    }

    public int getInc() {
        return (int) value;
    }

    @Override
    public String toString() {
        return "BsonTimestamp[value=" + getValue()
            + ", seconds=" + getTime()
            + ", inc=" + getInc()
            + "]";
    }

    @Override
    public int compareTo(BsonTimestamp other) {
        return Long.compareUnsigned(value, other.value);
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        BsonTimestamp other = (BsonTimestamp) o;
        return this.value == other.value;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(value);
    }

}
<file_sep>/memory-backend/src/test/java/de/bwaldvogel/mongo/backend/memory/MemoryBackendAggregationTest.java
package de.bwaldvogel.mongo.backend.memory;

import de.bwaldvogel.mongo.MongoBackend;
import de.bwaldvogel.mongo.backend.AbstractAggregationTest;

class MemoryBackendAggregationTest extends AbstractAggregationTest {

    @Override
    protected MongoBackend createBackend() throws Exception {
        return new MemoryBackend(clock);
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/stage/AddFieldsStage.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import java.util.Map.Entry;
import java.util.stream.Stream;

import de.bwaldvogel.mongo.backend.Missing;
import de.bwaldvogel.mongo.backend.aggregation.Expression;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.MongoServerError;

public class AddFieldsStage implements AggregationStage {

    private final Document addFields;

    public AddFieldsStage(Document addFields) {
        if (addFields.isEmpty()) {
            throw new MongoServerError(40177, "Invalid $addFields :: caused by :: specification must have at least one field");
        }
        this.addFields = addFields;
    }

    @Override
    public String name() {
        return "$addFields";
    }

    @Override
    public Stream<Document> apply(Stream<Document> stream) {
        return stream.map(this::projectDocument);
    }

    Document projectDocument(Document document) {
        Document clone = document.clone();
        putRecursively(clone, addFields);
        return clone;
    }

    private void putRecursively(Document target, Document source) {
        for (Entry<String, Object> entry : source.entrySet()) {
            String key = entry.getKey();
            Object value = Expression.evaluateDocument(entry.getValue(), target);
            if (value instanceof Document) {
                Document subDocument = (Document) target.compute(key, (k, oldValue) -> {
                    if (!(oldValue instanceof Document)) {
                        return new Document();
                    } else {
                        return oldValue;
                    }
                });
                putRecursively(subDocument, (Document) value);
            } else {
                if (value instanceof Missing) {
                    target.remove(key);
                } else {
                    target.put(key, value);
                }
            }
        }
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/Expression.java
package de.bwaldvogel.mongo.backend.aggregation;

import static de.bwaldvogel.mongo.backend.Missing.isNeitherNullNorMissing;
import static de.bwaldvogel.mongo.backend.Missing.isNullOrMissing;
import static de.bwaldvogel.mongo.backend.Utils.describeType;
import static de.bwaldvogel.mongo.bson.Json.toJsonValue;
import static java.util.Arrays.asList;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.time.temporal.IsoFields;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.OptionalDouble;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.regex.Pattern;

import de.bwaldvogel.mongo.backend.Assert;
import de.bwaldvogel.mongo.backend.BsonType;
import de.bwaldvogel.mongo.backend.CollectionUtils;
import de.bwaldvogel.mongo.backend.LinkedTreeSet;
import de.bwaldvogel.mongo.backend.Missing;
import de.bwaldvogel.mongo.backend.NumericUtils;
import de.bwaldvogel.mongo.backend.Utils;
import de.bwaldvogel.mongo.backend.ValueComparator;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.bson.ObjectId;
import de.bwaldvogel.mongo.exception.BadValueException;
import de.bwaldvogel.mongo.exception.ConversionFailureException;
import de.bwaldvogel.mongo.exception.ErrorCode;
import de.bwaldvogel.mongo.exception.FailedToOptimizePipelineError;
import de.bwaldvogel.mongo.exception.FailedToParseException;
import de.bwaldvogel.mongo.exception.MongoServerError;
import de.bwaldvogel.mongo.exception.UnsupportedConversionError;

public enum Expression implements ExpressionTraits {

    $abs {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return Utils.normalizeNumber(evaluateNumericValue(expressionValue, Math::abs));
        }
    },

    $add {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            boolean returnDate = false;
            Number sum = 0;
            for (Object value : expressionValue) {
                Object number = value;
                if (isNullOrMissing(number)) {
                    return null;
                }
                if (!(number instanceof Number) && !(number instanceof Instant)) {
                    throw new MongoServerError(16554,
                        name() + " only supports numeric or date types, not " + describeType(number));
                }
                if (number instanceof Instant) {
                    Instant instant = (Instant) number;
                    number = instant.toEpochMilli();
                    returnDate = true;
                }
                sum = NumericUtils.addNumbers(sum, (Number) number);
            }
            if (returnDate) {
                return Instant.ofEpochMilli(sum.longValue());
            }
            return sum;
        }
    },

    $and {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            for (Object value : expressionValue) {
                if (!Utils.isTrue(value)) {
                    return false;
                }
            }
            return true;
        }
    },

    $anyElementTrue {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            Object valueInCollection = requireSingleValue(expressionValue);
            if (!(valueInCollection instanceof Collection)) {
                throw new MongoServerError(17041,
                    name() + "'s argument must be an array, but is " + describeType(valueInCollection));
            }
            Collection<?> collectionInCollection = (Collection<?>) valueInCollection;
            for (Object value : collectionInCollection) {
                if (Utils.isTrue(value)) {
                    return true;
                }
            }
            return false;
        }
    },

    $allElementsTrue {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            Object parameter = requireSingleValue(expressionValue);
            if (!(parameter instanceof Collection)) {
                throw new MongoServerError(17040,
                    name() + "'s argument must be an array, but is " + describeType(parameter));
            }
            Collection<?> collectionInCollection = (Collection<?>) parameter;
            for (Object value : collectionInCollection) {
                if (!Utils.isTrue(value)) {
                    return false;
                }
            }
            return true;
        }
    },

    $arrayElemAt {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            TwoParameters parameters = requireTwoParameters(expressionValue);

            if (parameters.isAnyNull()) {
                return null;
            }

            Object firstValue = parameters.getFirst();
            Object secondValue = parameters.getSecond();
            if (!(firstValue instanceof List<?>)) {
                throw new MongoServerError(28689,
                    name() + "'s first argument must be an array, but is " + describeType(firstValue));
            }
            if (!(secondValue instanceof Number)) {
                throw new MongoServerError(28690,
                    name() + "'s second argument must be a numeric value, but is " + describeType(secondValue));
            }
            List<?> collection = (List<?>) firstValue;
            int index = ((Number) secondValue).intValue();
            if (index < 0) {
                index = collection.size() + index;
            }
            if (index < 0 || index >= collection.size()) {
                return Missing.getInstance();
            } else {
                return collection.get(index);
            }
        }
    },

    $arrayToObject {
        @Override
        Object apply(List<?> expressionValues, Document document) {
            Object values = requireSingleValue(expressionValues);
            if ((!(values instanceof Collection))) {
                throw new FailedToOptimizePipelineError(40386, name() + " requires an array input, found: " + describeType(values));
            }
            Document result = new Document();
            for (Object keyValueObject : (Collection<?>) values) {
                if (keyValueObject instanceof List) {
                    List<?> keyValue = (List<?>) keyValueObject;
                    if (keyValue.size() != 2) {
                        throw new FailedToOptimizePipelineError(40397, name() + " requires an array of size 2 arrays,found array of size: " + keyValue.size());
                    }
                    Object keyObject = keyValue.get(0);
                    if (!(keyObject instanceof String)) {
                        throw new FailedToOptimizePipelineError(40395, name() + " requires an array of key-value pairs, where the key must be of type string. Found key type: " + describeType(keyObject));
                    }
                    String key = (String) keyObject;
                    Object value = keyValue.get(1);
                    result.put(key, value);
                } else if (keyValueObject instanceof Document) {
                    Document keyValue = (Document) keyValueObject;
                    if (keyValue.size() != 2) {
                        throw new FailedToOptimizePipelineError(40392, name() + " requires an object keys of 'k' and 'v'. Found incorrect number of keys:" + keyValue.size());
                    }
                    if (!(keyValue.containsKey("k") && keyValue.containsKey("v"))) {
                        throw new FailedToOptimizePipelineError(40393, name() + " requires an object with keys 'k' and 'v'. Missing either or both keys from: " + keyValue.toString(true));
                    }
                    Object keyObject = keyValue.get("k");
                    if (!(keyObject instanceof String)) {
                        throw new FailedToOptimizePipelineError(40394, name() + " requires an object with keys 'k' and 'v', where the value of 'k' must be of type string. Found type: " + describeType(keyObject));
                    }
                    String key = (String) keyObject;
                    Object value = keyValue.get("v");
                    result.put(key, value);
                } else {
                    throw new FailedToOptimizePipelineError(40398, "Unrecognised input type format for " + name() + ": " + describeType(keyValueObject));
                }
            }
            return result;
        }
    },

    $avg {
        @Override
        Double apply(List<?> expressionValue, Document document) {
            Collection<?> values = getValues(expressionValue);
            OptionalDouble averageValue = values.stream()
                .filter(Number.class::isInstance)
                .map(Number.class::cast)
                .mapToDouble(Number::doubleValue)
                .average();
            if (averageValue.isPresent()) {
                return Double.valueOf(averageValue.getAsDouble());
            } else {
                return null;
            }
        }
    },

    $ceil {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateNumericValue(expressionValue, Math::ceil);
        }
    },

    $cmp {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateComparison(expressionValue);
        }
    },

    $concat {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            StringBuilder result = new StringBuilder();
            for (Object value : expressionValue) {
                if (isNullOrMissing(value)) {
                    return null;
                }
                if (!(value instanceof String)) {
                    throw new MongoServerError(16702,
                        name() + " only supports strings, not " + describeType(value));
                }
                result.append(value);
            }
            return result.toString();
        }
    },

    $concatArrays {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            List<Object> result = new ArrayList<>();
            for (Object value : expressionValue) {
                if (isNullOrMissing(value)) {
                    return null;
                }
                if (!(value instanceof Collection<?>)) {
                    throw new MongoServerError(28664,
                        name() + " only supports arrays, not " + describeType(value));
                }
                result.addAll((Collection<?>) value);
            }
            return result;
        }
    },

    $cond {
        @Override
        Object apply(Object expressionValue, Document document) {
            // document values need to be evaluated lazily
            List<Object> values = new ArrayList<>();
            if (!(expressionValue instanceof Collection)) {
                values.add(expressionValue);
            } else {
                values.addAll(((Collection<?>) expressionValue));
            }
            return apply(values, document);
        }

        @Override
        Object apply(List<?> expressionValue, Document document) {
            final Object ifExpression;
            final Object thenExpression;
            final Object elseExpression;

            if (expressionValue.size() == 1 && CollectionUtils.getSingleElement(expressionValue) instanceof Document) {
                Document condDocument = (Document) CollectionUtils.getSingleElement(expressionValue);
                List<String> requiredKeys = asList("if", "then", "else");
                for (String requiredKey : requiredKeys) {
                    if (!condDocument.containsKey(requiredKey)) {
                        throw new MongoServerError(17080, "Missing '" + requiredKey + "' parameter to " + name());
                    }
                }
                for (String key : condDocument.keySet()) {
                    if (!requiredKeys.contains(key)) {
                        throw new MongoServerError(17083, "Unrecognized parameter to " + name() + ": " + key);
                    }
                }

                ifExpression = condDocument.get("if");
                thenExpression = condDocument.get("then");
                elseExpression = condDocument.get("else");
            } else {
                requireCollectionInSize(expressionValue, 3);
                ifExpression = expressionValue.get(0);
                thenExpression = expressionValue.get(1);
                elseExpression = expressionValue.get(2);
            }

            if (Utils.isTrue(evaluate(ifExpression, document))) {
                return evaluate(thenExpression, document);
            } else {
                return evaluate(elseExpression, document);
            }
        }
    },

    $convert {
        @Override
        Object apply(List<?> expression, Document document) {
            Object expressionValue = CollectionUtils.getSingleElement(expression);
            if (!(expressionValue instanceof Document)) {
                throw new FailedToParseException(name() + " expects an object of named arguments but found: " + describeType(expressionValue));
            }
            Document convertDocument = (Document) expressionValue;

            List<String> supportedKeys = asList("input", "to", "onError", "onNull");
            for (String key : convertDocument.keySet()) {
                if (!supportedKeys.contains(key)) {
                    throw new FailedToParseException(name() + " found an unknown argument: " + key);
                }
            }

            List<String> requiredParameters = asList("input", "to");
            for (String requiredParameter : requiredParameters) {
                if (!convertDocument.containsKey(requiredParameter)) {
                    throw new FailedToParseException("Missing '" + requiredParameter + "' parameter to " + name());
                }
            }

            Object to = convertDocument.get("to");
            if (to == null) {
                return null;
            }

            Object inputValue = convertDocument.get("input");

            if (inputValue == null && convertDocument.containsKey("onNull")) {
                return convertDocument.get("onNull");
            }

            final BsonType bsonType = getBsonType(to);

            return convert(inputValue, bsonType, document, convertDocument);
        }

        private BsonType getBsonType(Object to) {
            try {
                if (to instanceof String) {
                    return BsonType.forString((String) to);
                } else if (to instanceof Integer) {
                    return BsonType.forNumber((Integer) to);
                } else if (to instanceof Number) {
                    throw new IllegalArgumentException("In $convert, numeric 'to' argument is not an integer");
                } else {
                    throw new IllegalArgumentException("$convert's 'to' argument must be a string or number, but is " + describeType(to));
                }
            } catch (BadValueException e) {
                throw new FailedToOptimizePipelineError(ErrorCode.BadValue, "Unknown type name: " + to);
            } catch (IllegalArgumentException e) {
                throw new FailedToOptimizePipelineError(ErrorCode.FailedToParse, e.getMessage());
            }
        }

        private Object convert(Object inputValue, BsonType bsonType, Document document, Document convertDocument) {
            try {
                switch (bsonType) {
                    case DOUBLE:
                        return $toDouble.apply(inputValue, document);
                    case STRING:
                        return $toString.apply(inputValue, document);
                    case OBJECT_ID:
                        return $toObjectId.apply(inputValue, document);
                    case BOOL:
                        return $toBool.apply(inputValue, document);
                    case DATE:
                        return $toDate.apply(inputValue, document);
                    case INT:
                        return $toInt.apply(inputValue, document);
                    case LONG:
                        return $toLong.apply(inputValue, document);
                    case OBJECT:
                    case ARRAY:
                    case BIN_DATA:
                    case NULL:
                    case REGEX:
                    case TIMESTAMP:
                    case DECIMAL128:
                    case MIN_KEY:
                    case MAX_KEY:
                    default:
                        throw new UnsupportedOperationException("Unsupported conversion to type " + bsonType);
                }
            } catch (MongoServerError e) {
                if (e.hasCode(ErrorCode.ConversionFailure)) {
                    if (convertDocument.containsKey("onError")) {
                        return convertDocument.get("onError");
                    }
                }
                throw e;
            }
        }
    },

    $dayOfMonth {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateDate(expressionValue, LocalDate::getDayOfMonth, document);
        }
    },

    $dayOfWeek {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateDate(expressionValue, date -> date.getDayOfWeek().getValue(), document);
        }
    },

    $dayOfYear {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateDate(expressionValue, LocalDate::getDayOfYear, document);
        }
    },

    $dateToString {
        @Override
        Object apply(Object expressionValue, Document document) {
            Document dateToStringDocument = requireDocument(expressionValue, 18629);

            // validate mandatory 'date' expression parameter
            if (!dateToStringDocument.containsKey("date")) {
                throw new MongoServerError(18628, "Missing 'date' parameter to " + name());
            }

            // validate unsupported parameters
            List<String> supportedKeys = asList("date", "format", "timezone", "onNull");
            for (String key : dateToStringDocument.keySet()) {
                if (!supportedKeys.contains(key)) {
                    throw new MongoServerError(18534, "Unrecognized parameter to " + name() + ": " + key);
                }
            }

            // validate optional 'format' parameter
            String format = "%Y-%m-%dT%H:%M:%S.%LZ";
            Object formatDocument = dateToStringDocument.get("format");
            if (formatDocument != null) {
                if (!(formatDocument instanceof String)) {
                    throw new MongoServerError(18533, name() + " requires that 'format' be a string, found: " + describeType(formatDocument) + " with value " + formatDocument.toString());
                }
                format = (String) formatDocument;
            }

            // validate optional 'timezone' parameter
            ZoneId timezone = ZoneId.of("UTC");
            Object timezoneValue = Expression.evaluate(dateToStringDocument.get("timezone"), document);
            if (timezoneValue != null) {
                try {
                    timezone = ZoneId.of(timezoneValue.toString());
                } catch (DateTimeException e) {
                    throw new MongoServerError(40485, name() + " unrecognized time zone identifier: " + timezoneValue);
                }
            }

            // optional parameter 'onNull'
            Object onNullValue = Expression.evaluate(dateToStringDocument.get("onNull"), document);

            // get zoned date time
            Object dateExpression = dateToStringDocument.get("date");
            Object dateValue = Expression.evaluate(dateExpression, document);
            if (Missing.isNullOrMissing(dateValue)) {
                return onNullValue;
            }
            if (!(dateValue instanceof Instant)) {
                throw new MongoServerError(16006, "can't convert from " + describeType(dateValue) + " to Date");
            }
            ZonedDateTime dateTime = ZonedDateTime.ofInstant((Instant) dateValue, timezone);

            // format
            return dateTime.format(builder(format).toFormatter());
        }

        private DateTimeFormatterBuilder builder(String format) {
            DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
            for (String part : format.split("(?=%.)")) {
                boolean hasFormatSpecifier = true;
                if (part.equals("%")) {
                    // empty format specifier
                    throw new MongoServerError(18535, "Unmatched '%' at end of $dateToString format string");
                } else if (part.startsWith("%d")) {
                    builder.appendValue(ChronoField.DAY_OF_MONTH, 2);
                } else if (part.startsWith("%G")) {
                    builder.appendValue(ChronoField.YEAR, 4);
                } else if (part.startsWith("%H")) {
                    builder.appendValue(ChronoField.HOUR_OF_DAY, 2);
                } else if (part.startsWith("%j")) {
                    builder.appendValue(ChronoField.DAY_OF_YEAR, 3);
                } else if (part.startsWith("%L")) {
                    builder.appendValue(ChronoField.MILLI_OF_SECOND, 3);
                } else if (part.startsWith("%m")) {
                    builder.appendValue(ChronoField.MONTH_OF_YEAR, 2);
                } else if (part.startsWith("%M")) {
                    builder.appendValue(ChronoField.MINUTE_OF_HOUR, 2);
                } else if (part.startsWith("%S")) {
                    builder.appendValue(ChronoField.SECOND_OF_MINUTE, 2);
                } else if (part.startsWith("%w")) {
                    throw new MongoServerError(18536, "Not yet supported format character '%w' in $dateToString format string");
                } else if (part.startsWith("%u")) {
                    builder.appendValue(ChronoField.DAY_OF_WEEK, 1);
                } else if (part.startsWith("%U")) {
                    throw new MongoServerError(18536, "Not yet supported format character '%U' in $dateToString format string");
                } else if (part.startsWith("%V")) {
                    builder.appendValue(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 2);
                } else if (part.startsWith("%Y")) {
                    builder.appendValue(ChronoField.YEAR, 4);
                } else if (part.startsWith("%z")) {
                    builder.appendOffset("+HHMM", "+0000");
                } else if (part.startsWith("%Z")) {
                    throw new MongoServerError(18536, "Not yet supported format character '%Z' in $dateToString format string");
                } else if (part.startsWith("%%")) {
                    builder.appendLiteral("%");
                } else if (part.startsWith("%")) {
                    // invalid format specifier
                    throw new MongoServerError(18536, "Invalid format character '" + part + "' in $dateToString format string");
                } else {
                    // literals (without format specifier)
                    hasFormatSpecifier = false;
                    builder.appendLiteral(part);
                }

                // append literals (after format specifier)
                if (hasFormatSpecifier && part.length() > 2) {
                    builder.appendLiteral(part.substring(2));
                }
            }
            return builder;
        }

        @Override
        Object apply(List<?> expressionValue, Document document) {
            throw new UnsupportedOperationException("must not be invoked");
        }
    },

    $divide {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            TwoNumericParameters parameters = requireTwoNumericParameters(expressionValue, 16609);

            if (parameters == null) {
                return null;
            }

            double a = parameters.getFirstAsDouble();
            double b = parameters.getSecondAsDouble();
            if (Double.compare(b, 0.0) == 0) {
                throw new MongoServerError(16608, "can't " + name() + " by zero");
            }
            return a / b;
        }
    },

    $eq {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateComparison(expressionValue, v -> v == 0);
        }
    },

    $exp {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateNumericValue(expressionValue, Math::exp);
        }
    },

    $filter {
        @Override
        Object apply(Object expressionValue, Document document) {
            Document filterExpression = requireDocument(expressionValue, 28646);
            List<String> requiredKeys = asList("input", "cond");
            for (String requiredKey : requiredKeys) {
                if (!filterExpression.containsKey(requiredKey)) {
                    throw new MongoServerError(28648, "Missing '" + requiredKey + "' parameter to " + name());
                }
            }

            for (String key : filterExpression.keySet()) {
                if (!asList("input", "cond", "as").contains(key)) {
                    throw new MongoServerError(28647, "Unrecognized parameter to " + name() + ": " + key);
                }
            }

            Object input = evaluate(filterExpression.get("input"), document);
            Object as = evaluate(filterExpression.getOrDefault("as", "this"), document);
            if (!(as instanceof String) || Objects.equals(as, "")) {
                throw new MongoServerError(16866, "empty variable names are not allowed");
            }
            if (Missing.isNullOrMissing(input)) {
                return null;
            }

            if (!(input instanceof Collection)) {
                throw new MongoServerError(28651, "input to " + name() + " must be an array not " + describeType(input));
            }

            Collection<?> inputCollection = (Collection<?>) input;

            String key = "$" + as;
            Document documentForCondition = document.clone();
            Assert.isFalse(documentForCondition.containsKey(key), () -> "Document already contains '" + key + "'");
            List<Object> result = new ArrayList<>();
            for (Object inputValue : inputCollection) {
                Object evaluatedInputValue = evaluate(inputValue, document);
                documentForCondition.put(key, evaluatedInputValue);
                if (Utils.isTrue(evaluate(filterExpression.get("cond"), documentForCondition))) {
                    result.add(evaluatedInputValue);
                }
            }
            return result;
        }

        @Override
        Object apply(List<?> expressionValue, Document document) {
            throw new UnsupportedOperationException("must not be invoked");
        }
    },

    $floor {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateNumericValue(expressionValue, a -> toIntOrLong(Math.floor(a)));
        }
    },

    $gt {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateComparison(expressionValue, v -> v > 0);
        }
    },

    $gte {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateComparison(expressionValue, v -> v >= 0);
        }
    },

    $hour {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateTime(expressionValue, LocalTime::getHour, document);
        }
    },

    $ifNull {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            TwoParameters parameters = requireTwoParameters(expressionValue);
            Object expression = parameters.getFirst();
            if (isNeitherNullNorMissing(expression)) {
                return expression;
            } else {
                return parameters.getSecond();
            }
        }
    },

    $in {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            TwoParameters parameters = requireTwoParameters(expressionValue);
            Object needle = parameters.getFirst();
            Object haystack = parameters.getSecond();

            if (!(haystack instanceof Collection)) {
                throw new MongoServerError(40081, name() + " requires an array as a second argument, found: " + describeType(haystack));
            }

            return ((Collection<?>) haystack).contains(needle);
        }
    },

    $indexOfArray {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            Object first = assertTwoToFourArguments(expressionValue);
            if (first == null) {
                return null;
            }
            if (!(first instanceof List<?>)) {
                throw new MongoServerError(40090,
                    name() + " requires an array as a first argument, found: " + describeType(first));
            }
            List<?> elementsToSearchIn = (List<?>) first;

            Range range = indexOf(expressionValue, elementsToSearchIn.size());

            elementsToSearchIn = elementsToSearchIn.subList(range.getStart(), range.getEnd());
            int index = elementsToSearchIn.indexOf(expressionValue.get(1));
            if (index >= 0) {
                return index + range.getStart();
            }
            return index;
        }

    },

    $indexOfBytes {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateIndexOf(expressionValue, this::toList,
                40091, 40092);
        }

        private List<Byte> toList(String input) {
            List<Byte> bytes = new ArrayList<>();
            for (byte value : input.getBytes(StandardCharsets.UTF_8)) {
                bytes.add(value);
            }
            return bytes;
        }

    },

    $indexOfCP {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateIndexOf(expressionValue, this::toList,
                40093, 40094);
        }

        private List<Character> toList(String input) {
            List<Character> characters = new ArrayList<>();
            for (char value : input.toCharArray()) {
                characters.add(value);
            }
            return characters;
        }

    },

    $isArray {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            Object value = requireSingleValue(expressionValue);
            return (value instanceof List);
        }
    },

    $literal {
        @Override
        Object apply(Object expressionValue, Document document) {
            return expressionValue;
        }

        @Override
        Object apply(List<?> expressionValue, Document document) {
            throw new UnsupportedOperationException("must not be invoked");
        }
    },

    $ln {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateNumericValue(expressionValue, Math::log);
        }
    },

    $log {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateNumericValue(expressionValue, Math::log);
        }
    },

    $log10 {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateNumericValue(expressionValue, Math::log10);
        }
    },

    $lt {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateComparison(expressionValue, v -> v < 0);
        }
    },

    $lte {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateComparison(expressionValue, v -> v <= 0);
        }
    },


    $map {
        @Override
        Object apply(Object expressionValue, Document document) {
            Document filterExpression = requireDocument(expressionValue, 16878);
            List<String> requiredKeys = asList("input", "in");
            for (String requiredKey : requiredKeys) {
                if (!filterExpression.containsKey(requiredKey)) {
                    throw new MongoServerError(16882, "Missing '" + requiredKey + "' parameter to " + name());
                }
            }

            for (String key : filterExpression.keySet()) {
                if (!asList("input", "in", "as").contains(key)) {
                    throw new MongoServerError(16879, "Unrecognized parameter to " + name() + ": " + key);
                }
            }

            Object input = evaluate(filterExpression.get("input"), document);
            Object as = evaluate(filterExpression.getOrDefault("as", "this"), document);
            if (!(as instanceof String) || Objects.equals(as, "")) {
                throw new MongoServerError(16866, "empty variable names are not allowed");
            }
            if (Missing.isNullOrMissing(input)) {
                return null;
            }

            if (!(input instanceof Collection)) {
                throw new MongoServerError(16883, "input to " + name() + " must be an array not " + describeType(input));
            }

            Collection<?> inputCollection = (Collection<?>) input;

            String key = "$" + as;
            Document documentForCondition = document.clone();
            Assert.isFalse(documentForCondition.containsKey(key), () -> "Document already contains '" + key + "'");
            List<Object> result = new ArrayList<>();
            for (Object inputValue : inputCollection) {
                Object evaluatedInputValue = evaluate(inputValue, document);
                documentForCondition.put(key, evaluatedInputValue);
                result.add(evaluate(filterExpression.get("in"), documentForCondition));
            }
            return result;
        }

        @Override
        Object apply(List<?> expressionValue, Document document) {
            throw new UnsupportedOperationException("must not be invoked");
        }
    },

    $reduce {
        @Override
        Object apply(Object expressionValue, Document document) {
            Document reduceExpression = requireDocument(expressionValue, 40075);
            List<String> requiredKeys = asList("input", "initialValue", "in");
            for (String requiredKey : requiredKeys) {
                if (!reduceExpression.containsKey(requiredKey)) {
                    throw new MongoServerError(40079, "Missing '" + requiredKey + "' parameter to " + name());
                }
            }

            for (String key : reduceExpression.keySet()) {
                if (!asList("input", "initialValue", "in").contains(key)) {
                    throw new MongoServerError(40076, "Unrecognized parameter to " + name() + ": " + key);
                }
            }

            Object input = evaluate(reduceExpression.get("input"), document);
            Object initialValue = evaluate(reduceExpression.get("initialValue"), document);

            if (Missing.isNullOrMissing(input)) {
                return null;
            }

            if (!(input instanceof Collection)) {
                throw new MongoServerError(40080, "input to " + name() + " must be an array not " + describeType(input));
            }

            Collection<?> inputCollection = (Collection<?>) input;

            final String thisKey = "$this";
            final String valueKey = "$value";
            Document documentForReduce = document.clone();
            Assert.isFalse(documentForReduce.containsKey(thisKey), () -> "Document already contains '" + thisKey + "'");
            Assert.isFalse(documentForReduce.containsKey(valueKey), () -> "Document already contains '" + valueKey + "'");
            Object result = initialValue;
            for (Object inputValue : inputCollection) {
                Object evaluatedInputValue = evaluate(inputValue, document);
                documentForReduce.put(thisKey, evaluatedInputValue);
                documentForReduce.put(valueKey, result);
                result = evaluate(reduceExpression.get("in"), documentForReduce);
            }

            return result;
        }

        @Override
        Object apply(List<?> expressionValue, Document document) {
            throw new UnsupportedOperationException("must not be invoked");
        }
    },

    $max {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            Collection<?> values = getValues(expressionValue);
            return values.stream()
                .filter(Missing::isNeitherNullNorMissing)
                .max(ValueComparator.asc())
                .orElse(null);
        }
    },

    $mergeObjects {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            Document result = new Document();
            for (Object value : expressionValue) {
                if (isNullOrMissing(value)) {
                    continue;
                }
                if (!(value instanceof Document)) {
                    throw new MongoServerError(40400,
                        "$mergeObjects requires object inputs, but input " + toJsonValue(value) + " is of type " + describeType(value));
                }
                result.merge((Document) value);
            }
            return result;
        }
    },

    $min {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            Collection<?> values = getValues(expressionValue);
            return values.stream()
                .filter(Missing::isNeitherNullNorMissing)
                .min(ValueComparator.asc())
                .orElse(null);
        }
    },

    $minute {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateTime(expressionValue, LocalTime::getMinute, document);
        }
    },

    $mod {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            TwoNumericParameters parameters = requireTwoNumericParameters(expressionValue, 16611);
            if (parameters == null) {
                return null;
            }
            double a = parameters.getFirstAsDouble();
            double b = parameters.getSecondAsDouble();
            return a % b;
        }
    },

    $month {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateDate(expressionValue, date -> date.getMonth().getValue(), document);
        }
    },

    $multiply {
        @Override
        Number apply(List<?> expressionValue, Document document) {
            TwoNumericParameters parameters = requireTwoNumericParameters(expressionValue, 16555);

            if (parameters == null) {
                return null;
            }

            Number first = parameters.getFirst();
            Number second = parameters.getSecond();
            return NumericUtils.multiplyNumbers(first, second);
        }

    },


    $ne {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateComparison(expressionValue, v -> v != 0);
        }
    },

    $not {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            Object value = requireSingleValue(expressionValue);
            return !Utils.isTrue(value);
        }
    },

    $objectToArray {
        @Override
        List<Document> apply(List<?> expressionValue, Document document) {
            Object value = requireSingleValue(expressionValue);
            if (!(value instanceof Document)) {
                throw new MongoServerError(ErrorCode._40390, name() + " requires a document input, found: " + describeType(value));
            }
            List<Document> result = new ArrayList<>();
            for (Entry<String, Object> entry : ((Document) value).entrySet()) {
                Document keyValue = new Document();
                keyValue.append("k", entry.getKey());
                keyValue.append("v", entry.getValue());
                result.add(keyValue);
            }
            return result;
        }
    },

    $or {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            for (Object value : expressionValue) {
                if (Utils.isTrue(value)) {
                    return true;
                }
            }
            return false;
        }
    },

    $pow {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            TwoParameters parameters = requireTwoParameters(expressionValue);
            if (parameters.isAnyNull()) {
                return null;
            }

            Object base = parameters.getFirst();
            Object exponent = parameters.getSecond();

            if (!(base instanceof Number)) {
                throw new MongoServerError(28762,
                    name() + "'s base must be numeric, not " + describeType(base));
            }

            if (!(exponent instanceof Number)) {
                throw new MongoServerError(28763,
                    name() + "'s exponent must be numeric, not " + describeType(exponent));
            }

            double a = ((Number) base).doubleValue();
            double b = ((Number) exponent).doubleValue();
            return Math.pow(a, b);
        }
    },

    $rand {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            if (expressionValue.size() > 1) {
                throw new MongoServerError(3040501, name() + " does not currently accept arguments");
            } else if (expressionValue.size() == 1) {
                Object parameter = expressionValue.get(0);
                if (!(parameter instanceof Document)) {
                    throw new MongoServerError(10065, "invalid parameter: expected an object (" + name() + ")");
                }
                Document params = (Document) parameter;
                if (!params.isEmpty()) {
                    throw new MongoServerError(3040501, name() + " does not currently accept arguments");
                }
            }
            return random.nextDouble();
        }
    },

    $range {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            if (expressionValue.size() < 2 || expressionValue.size() > 3) {
                throw new MongoServerError(28667, "Expression " + name() + " takes at least 2 arguments, and at most 3, but " + expressionValue.size() + " were passed in.");
            }

            Object first = expressionValue.get(0);
            Object second = expressionValue.get(1);

            int start = toInt(first, 34443, 34444, "starting value");
            int end = toInt(second, 34445, 34446, "ending value");

            final int step;
            if (expressionValue.size() > 2) {
                Object third = expressionValue.get(2);
                step = toInt(third, 34447, 34448, "step value");
                if (step == 0) {
                    throw new MongoServerError(34449, name() + " requires a non-zero step value");
                }
            } else {
                step = 1;
            }

            List<Integer> values = new ArrayList<>();
            if (step > 0) {
                for (int i = start; i < end; i += step) {
                    values.add(i);
                }
            } else {
                for (int i = start; i > end; i -= Math.abs(step)) {
                    values.add(i);
                }
            }
            return values;
        }

        private int toInt(Object object, int errorCodeIfNotANumber, int errorCodeIfNonInt, String errorMessage) {
            if (!(object instanceof Number)) {
                throw new MongoServerError(errorCodeIfNotANumber, name() + " requires a numeric " + errorMessage + ", found value of type: " + describeType(object));
            }
            Number number = (Number) object;
            int value = number.intValue();
            if (number.doubleValue() != value) {
                throw new MongoServerError(errorCodeIfNonInt,
                    name() + " requires a " + errorMessage + " that can be represented as a 32-bit integer, found value: " + number);
            }
            return value;
        }
    },

    $reverseArray {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            Object value = requireSingleValue(expressionValue);
            if (isNullOrMissing(value)) {
                return null;
            }
            if (!(value instanceof Collection<?>)) {
                throw new MongoServerError(34435,
                    "The argument to " + name() + " must be an array, but was of type: " + describeType(value));
            }

            List<?> list = new ArrayList<>((Collection<?>) value);
            Collections.reverse(list);
            return list;
        }
    },

    $second {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateTime(expressionValue, LocalTime::getSecond, document);
        }
    },

    $setDifference {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            TwoParameters parameters = requireTwoParameters(expressionValue);

            if (parameters.isAnyNull()) {
                return null;
            }

            Object first = parameters.getFirst();
            Object second = parameters.getSecond();

            if (!(first instanceof Collection)) {
                throw new MongoServerError(17048,
                    "both operands of " + name() + " must be arrays. First argument is of type: " + describeType(first));
            }

            if (!(second instanceof Collection)) {
                throw new MongoServerError(17049,
                    "both operands of " + name() + " must be arrays. First argument is of type: " + describeType(second));
            }

            Set<Object> result = new LinkedTreeSet<>((Collection<?>) first);
            result.removeAll((Collection<?>) second);
            return result;
        }

    },

    $setEquals {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            if (expressionValue.size() < 2) {
                throw new MongoServerError(17045, name() + " needs at least two arguments had: " + expressionValue.size());
            }

            Set<?> objects = null;
            for (Object value : expressionValue) {
                if (!(value instanceof Collection)) {
                    throw new MongoServerError(17044, "All operands of " + name() + " must be arrays. One argument is of type: " + describeType(value));
                }
                Set<?> setValue = new LinkedTreeSet<>((Collection<?>) value);
                if (objects == null) {
                    objects = setValue;
                } else {
                    if (!objects.containsAll(setValue) || !setValue.containsAll(objects)) {
                        return false;
                    }
                }
            }
            return true;
        }
    },

    $setIntersection {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            Set<?> result = null;
            for (Object value : expressionValue) {
                if (isNullOrMissing(value)) {
                    return null;
                }
                if (!(value instanceof Collection)) {
                    throw new MongoServerError(17047, "All operands of " + name() + " must be arrays. One argument is of type: " + describeType(value));
                }
                Collection<?> values = (Collection<?>) value;
                if (result == null) {
                    result = new LinkedTreeSet<>(values);
                } else {
                    result.retainAll(values);
                }
            }
            if (result == null) {
                return Collections.emptySet();
            }
            return result;
        }
    },

    $setIsSubset {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            TwoParameters parameters = requireTwoParameters(expressionValue);
            Object first = parameters.getFirst();
            Object second = parameters.getSecond();

            if (!(first instanceof Collection<?>)) {
                throw new MongoServerError(17046, "both operands of " + name() + " must be arrays. First argument is of type: " + describeType(first));
            }

            if (!(second instanceof Collection<?>)) {
                throw new MongoServerError(17042, "both operands of " + name() + " must be arrays. Second argument is of type: " + describeType(second));
            }

            Set<?> one = new LinkedTreeSet<>((Collection<?>) first);
            Set<?> other = new LinkedTreeSet<>((Collection<?>) second);
            return other.containsAll(one);
        }
    },

    $setUnion {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            Set<Object> result = new TreeSet<>(ValueComparator.asc());
            for (Object value : expressionValue) {
                if (isNullOrMissing(value)) {
                    return null;
                }
                if (!(value instanceof Collection<?>)) {
                    throw new MongoServerError(17043,
                        "All operands of " + name() + " must be arrays. One argument is of type: " + describeType(value));
                }
                result.addAll((Collection<?>) value);
            }
            return result;
        }
    },

    $size {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            Object value = requireSingleValue(expressionValue);
            Collection<?> collection = requireArray(17124, value);
            return collection.size();
        }
    },

    $slice {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            if (expressionValue.size() < 2 || expressionValue.size() > 3) {
                throw new MongoServerError(28667, "Expression " + name() + " takes at least 2 arguments, and at most 3, but " + expressionValue.size() + " were passed in.");
            }

            Object first = expressionValue.get(0);
            if (isNullOrMissing(first)) {
                return null;
            }
            if (!(first instanceof List)) {
                throw new MongoServerError(28724, "First argument to " + name() + " must be an array, but is of type: " + describeType(first));
            }
            List<?> list = (List<?>) first;

            Object second = expressionValue.get(1);
            if (!(second instanceof Number)) {
                throw new MongoServerError(28725, "Second argument to " + name() + " must be a numeric value, but is of type: " + describeType(second));
            }

            final List<?> result;
            if (expressionValue.size() > 2) {
                Object third = expressionValue.get(2);
                if (!(third instanceof Number)) {
                    throw new MongoServerError(28725, "Third argument to " + name() + " must be numeric, but is of type: " + describeType(third));
                }

                Number number = (Number) third;
                if (number.intValue() < 0) {
                    throw new MongoServerError(28729, "Third argument to " + name() + " must be positive: " + third);
                }

                int position = ((Number) second).intValue();
                final int offset;
                if (position >= 0) {
                    offset = Math.min(position, list.size());
                } else {
                    offset = Math.max(0, list.size() + position);
                }

                result = list.subList(offset, Math.min(offset + number.intValue(), list.size()));
            } else {
                int n = ((Number) second).intValue();
                if (n >= 0) {
                    result = list.subList(0, Math.min(n, list.size()));
                } else {
                    result = list.subList(Math.max(0, list.size() + n), list.size());
                }
            }

            return result;
        }
    },

    $split {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            TwoParameters parameters = requireTwoParameters(expressionValue);
            Object string = parameters.getFirst();
            Object delimiter = parameters.getSecond();

            if (isNullOrMissing(string)) {
                return null;
            }

            if (!(string instanceof String)) {
                throw new MongoServerError(40085,
                    name() + " requires an expression that evaluates to a string as a first argument, found: " + describeType(string));
            }
            if (!(delimiter instanceof String)) {
                throw new MongoServerError(40086,
                    name() + " requires an expression that evaluates to a string as a second argument, found: " + describeType(delimiter));
            }

            String[] values = ((String) string).split(Pattern.quote((String) delimiter), -1);
            return List.of(values);
        }
    },

    $subtract {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            TwoParameters parameters = requireTwoParameters(expressionValue);
            Object one = parameters.getFirst();
            Object other = parameters.getSecond();

            if (isNullOrMissing(one) || isNullOrMissing(other)) {
                return null;
            }

            if (one instanceof Number && other instanceof Number) {
                return NumericUtils.subtractNumbers((Number) one, (Number) other);
            }

            if (one instanceof Instant) {
                // subtract two instants (returns the difference in milliseconds)
                if (other instanceof Instant) {
                    return ((Instant) one).toEpochMilli() - ((Instant) other).toEpochMilli();
                }
                // subtract milliseconds from instant
                if (other instanceof Number) {
                    return Instant.ofEpochMilli(((Instant) one).toEpochMilli() - ((Number) other).longValue());
                }
            }

            throw new MongoServerError(16556, "cant " + name() + " a " + describeType(one) + " from a " + describeType(other));
        }
    },

    $sum {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            if (expressionValue.size() == 1) {
                Object singleValue = CollectionUtils.getSingleElement(expressionValue);
                if (singleValue instanceof Collection<?>) {
                    return apply(singleValue, document);
                }
            }
            Number sum = 0;
            for (Object value : expressionValue) {
                if (value instanceof Number) {
                    sum = NumericUtils.addNumbers(sum, (Number) value);
                }
            }
            return sum;
        }
    },

    $sqrt {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateNumericValue(expressionValue, Math::sqrt);
        }
    },

    $strLenBytes {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            String string = requireSingleStringValue(expressionValue);
            return string.getBytes(StandardCharsets.UTF_8).length;
        }
    },

    $strLenCP {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            String string = requireSingleStringValue(expressionValue);
            return string.length();
        }
    },

    $substr {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return $substrBytes.apply(expressionValue, document);
        }
    },

    $substrBytes {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            requireCollectionInSize(expressionValue, 3);
            String value = convertToString(expressionValue.get(0));
            if (value == null || value.isEmpty()) {
                return "";
            }

            byte[] bytes = value.getBytes(StandardCharsets.UTF_8);

            Object startValue = expressionValue.get(1);
            if (!(startValue instanceof Number)) {
                throw new FailedToOptimizePipelineError(16034, name() + ":  starting index must be a numeric type (is BSON type " + describeType(startValue) + ")");
            }
            int startIndex = Math.max(0, ((Number) startValue).intValue());
            startIndex = Math.min(bytes.length, startIndex);

            Object lengthValue = expressionValue.get(2);
            if (!(lengthValue instanceof Number)) {
                throw new FailedToOptimizePipelineError(16035, name() + ":  length must be a numeric type (is BSON type " + describeType(lengthValue) + ")");
            }
            int length = ((Number) lengthValue).intValue();
            if (length < 0) {
                length = bytes.length - startIndex;
            }
            length = Math.min(bytes.length, length);
            return new String(bytes, startIndex, length, StandardCharsets.UTF_8);
        }
    },

    $substrCP {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            requireCollectionInSize(expressionValue, 3);
            String value = convertToString(expressionValue.get(0));
            if (value == null || value.isEmpty()) {
                return "";
            }
            Object startValue = expressionValue.get(1);
            if (!(startValue instanceof Number)) {
                throw new FailedToOptimizePipelineError(34450, name() + ": starting index must be a numeric type (is BSON type " + describeType(startValue) + ")");
            }
            int startIndex = Math.max(0, ((Number) startValue).intValue());
            startIndex = Math.min(value.length(), startIndex);

            Object lengthValue = expressionValue.get(2);
            if (!(lengthValue instanceof Number)) {
                throw new FailedToOptimizePipelineError(34452, name() + ": length must be a numeric type (is BSON type " + describeType(lengthValue) + ")");
            }
            int length = ((Number) lengthValue).intValue();
            if (length < 0) {
                length = value.length() - startIndex;
            }
            int endIndex = Math.min(value.length(), startIndex + length);
            return value.substring(startIndex, endIndex);
        }
    },

    $toBool {
        @Override
        Boolean apply(List<?> expressionValue, Document document) {
            Object value = requireSingleValue(expressionValue);
            if (Missing.isNullOrMissing(value)) {
                return null;
            } else if (value instanceof Number) {
                Number number = (Number) value;
                return number.doubleValue() != 0.0;
            } else if (value instanceof Boolean) {
                return (Boolean) value;
            } else {
                return true;
            }
        }
    },

    $toDate {
        private final DateTimeFormatter YEAR_MONTH = DateTimeFormatter.ofPattern("yyyy-MM", Locale.ROOT);
        private final DateTimeFormatter OFFSET_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]XXXX", Locale.ROOT);

        @Override
        Instant apply(List<?> expressionValue, Document document) {
            Object value = requireSingleValue(expressionValue);
            if (Missing.isNullOrMissing(value)) {
                return null;
            } else if (value instanceof Long || value instanceof Double) {
                Number number = (Number) value;
                return Instant.ofEpochMilli(number.longValue());
            } else if (value instanceof Instant) {
                return (Instant) value;
            } else if (value instanceof String) {
                String dateString = (String) value;
                try {
                    return Instant.parse(dateString);
                } catch (DateTimeParseException e1) {
                    try {
                        return LocalDate.parse(dateString).atStartOfDay(ZoneOffset.UTC).toInstant();
                    } catch (DateTimeParseException e2) {
                        try {
                            return ZonedDateTime.parse(dateString, OFFSET_DATE_TIME).toInstant();
                        } catch (DateTimeParseException e3) {
                            try {
                                TemporalAccessor temporalAccessor = YEAR_MONTH.parse(dateString);
                                int year = temporalAccessor.get(ChronoField.YEAR);
                                int month = temporalAccessor.get(ChronoField.MONTH_OF_YEAR);
                                return LocalDate.of(year, month, 1).atStartOfDay(ZoneOffset.UTC).toInstant();
                            } catch (DateTimeParseException e) {
                                throw new ConversionFailureException("Error parsing date string '" + dateString + "';");
                            }
                        }
                    }
                }
            } else {
                throw new UnsupportedConversionError(value, Instant.class);
            }
        }
    },

    $toDouble {
        @Override
        Double apply(List<?> expressionValue, Document document) {
            Object value = requireSingleValue(expressionValue);
            if (Missing.isNullOrMissing(value)) {
                return null;
            } else if (value instanceof Number) {
                Number number = (Number) value;
                return number.doubleValue();
            } else if (value instanceof Boolean) {
                Boolean booleanValue = (Boolean) value;
                return booleanValue.booleanValue() ? 1.0 : 0.0;
            } else if (value instanceof Instant) {
                Instant instant = (Instant) value;
                return (double) instant.toEpochMilli();
            } else if (value instanceof String) {
                String string = (String) value;
                try {
                    return Double.valueOf(string);
                } catch (NumberFormatException e) {
                    throw new ConversionFailureException("Failed to parse number '" + value
                        + "' in $convert with no onError value: Did not consume whole number.");
                }
            } else {
                throw new UnsupportedConversionError(value, Double.class);
            }
        }
    },

    $toInt {
        @Override
        Integer apply(List<?> expressionValue, Document document) {
            Object value = requireSingleValue(expressionValue);
            if (Missing.isNullOrMissing(value)) {
                return null;
            } else if (value instanceof Number) {
                Number number = (Number) value;
                return number.intValue();
            } else if (value instanceof Boolean) {
                Boolean booleanValue = (Boolean) value;
                return booleanValue.booleanValue() ? 1 : 0;
            } else if (value instanceof String) {
                String string = (String) value;
                try {
                    return Integer.valueOf(string);
                } catch (NumberFormatException e) {
                    throw new ConversionFailureException("Failed to parse number '" + value
                        + "' in $convert with no onError value.");
                }
            } else {
                throw new UnsupportedConversionError(value, Integer.class);
            }
        }
    },

    $toLong {
        @Override
        Long apply(List<?> expressionValue, Document document) {
            Object value = requireSingleValue(expressionValue);
            if (Missing.isNullOrMissing(value)) {
                return null;
            } else if (value instanceof Number) {
                Number number = (Number) value;
                return number.longValue();
            } else if (value instanceof Boolean) {
                Boolean booleanValue = (Boolean) value;
                return booleanValue.booleanValue() ? 1L : 0L;
            } else if (value instanceof Instant) {
                Instant instant = (Instant) value;
                return instant.toEpochMilli();
            } else if (value instanceof String) {
                String string = (String) value;
                try {
                    return Long.valueOf(string);
                } catch (NumberFormatException e) {
                    throw new ConversionFailureException("Failed to parse number '" + value
                        + "' in $convert with no onError value.");
                }
            } else {
                throw new UnsupportedConversionError(value, Long.class);
            }
        }
    },

    $toLower {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateString(expressionValue, String::toLowerCase);
        }
    },

    $toObjectId {
        @Override
        ObjectId apply(List<?> expressionValue, Document document) {
            Object value = requireSingleValue(expressionValue);
            if (Missing.isNullOrMissing(value)) {
                return null;
            } else if (value instanceof String) {
                String string = (String) value;
                try {
                    return new ObjectId(string);
                } catch (RuntimeException e) {
                    throw new ConversionFailureException("Failed to parse objectId '" + value
                        + "' in $convert with no onError value.");
                }
            } else {
                throw new UnsupportedConversionError(value, ObjectId.class);
            }
        }
    },

    $toUpper {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateString(expressionValue, String::toUpperCase);
        }
    },

    $toString {
        @Override
        String apply(List<?> expressionValue, Document document) {
            return evaluateString(expressionValue, Function.identity());
        }
    },

    $trunc {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateNumericValue(expressionValue, a -> toIntOrLong(a.longValue()));
        }
    },

    $year {
        @Override
        Object apply(List<?> expressionValue, Document document) {
            return evaluateDate(expressionValue, LocalDate::getYear, document);
        }
    };

    private static final Set<String> KEYWORD_EXPRESSIONS = new HashSet<>(asList("$$PRUNE", "$$KEEP", "$$DESCEND"));

    private static final SecureRandom random = new SecureRandom();

    private static Collection<?> getValues(List<?> expressionValue) {
        Collection<?> values = expressionValue;
        if (expressionValue.size() == 1) {
            if (expressionValue.get(0) instanceof Collection) {
                values = (Collection<?>) expressionValue.get(0);
            }
        }
        return values;
    }

    Object apply(Object expressionValue, Document document) {
        List<Object> evaluatedValues = new ArrayList<>();
        if (!(expressionValue instanceof Collection)) {
            evaluatedValues.add(evaluate(expressionValue, document));
        } else {
            for (Object value : ((Collection<?>) expressionValue)) {
                evaluatedValues.add(evaluate(value, document));
            }
        }
        return apply(evaluatedValues, document);
    }

    abstract Object apply(List<?> expressionValue, Document document);

    public static Object evaluateDocument(Object documentWithExpression, Document document) {
        Object evaluatedValue = evaluate(documentWithExpression, document);
        if (evaluatedValue instanceof Document) {
            Document projectedDocument = (Document) evaluatedValue;
            Document result = new Document();
            for (Entry<String, Object> entry : projectedDocument.entrySet()) {
                String field = entry.getKey();
                Object expression = entry.getValue();
                Object value = evaluate(expression, document);
                if (!(value instanceof Missing)) {
                    result.put(field, value);
                }
            }
            return result;
        } else {
            return evaluatedValue;
        }
    }

    static Object evaluate(Object expression, Document document) {
        if (expression instanceof String && ((String) expression).startsWith("$")) {
            if (KEYWORD_EXPRESSIONS.contains(expression)) {
                return expression;
            }
            String value = ((String) expression).substring(1);
            if (value.startsWith("$")) {
                final String variableName;
                if (value.contains(".")) {
                    variableName = value.substring(0, value.indexOf('.'));
                } else {
                    variableName = value;
                }

                Object variableValue = Utils.getSubdocumentValue(document, variableName);
                if (variableValue instanceof Missing) {
                    if (variableName.equals("$ROOT")) {
                        variableValue = document;
                    } else if (variableName.equals("$NOW")) {
                        variableValue = Instant.now();
                    } else {
                        throw new MongoServerError(17276, "Use of undefined variable: " + variableName.substring(1));
                    }
                }

                if (variableValue instanceof String) {
                    String variableValueString = (String) variableValue;
                    if (variableValueString.startsWith("$")) {
                        variableValue = evaluate(variableValue, document);
                    }
                }

                if (!value.equals(variableName)) {
                    String path = value.substring(variableName.length() + 1);
                    return Utils.getSubdocumentValue((Document) variableValue, path);
                } else {
                    return variableValue;
                }
            }
            return Utils.getSubdocumentValueCollectionAware(document, value);
        } else if (expression instanceof Document) {
            return evaluateDocumentExpression((Document) expression, document);
        } else {
            return expression;
        }
    }

    private static Object evaluateDocumentExpression(Document expression, Document document) {
        Document result = new Document();
        for (Entry<String, Object> entry : expression.entrySet()) {
            String expressionKey = entry.getKey();
            Object expressionValue = entry.getValue();
            if (expressionKey.startsWith("$")) {
                if (expression.keySet().size() > 1) {
                    throw new MongoServerError(15983, "An object representing an expression must have exactly one field: " + expression);
                }

                final Expression exp;
                try {
                    exp = valueOf(expressionKey);
                } catch (IllegalArgumentException ex) {
                    throw new MongoServerError(168, "InvalidPipelineOperator", "Unrecognized expression '" + expressionKey + "'");
                }
                return exp.apply(expressionValue, document);
            } else {
                result.put(expressionKey, evaluate(expressionValue, document));
            }
        }
        return result;
    }

    private static Number toIntOrLong(double value) {
        long number = (long) value;
        if (number < Integer.MIN_VALUE || number > Integer.MAX_VALUE) {
            return number;
        } else {
            return Math.toIntExact(number);
        }
    }

}
<file_sep>/examples/src/main/java/SimpleJUnit5WithLegacyClientTest.java
import static org.assertj.core.api.Assertions.assertThat;

import java.net.InetSocketAddress;

import org.bson.Document;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.mongodb.MongoClient;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoCollection;

import de.bwaldvogel.mongo.MongoServer;
import de.bwaldvogel.mongo.backend.memory.MemoryBackend;

class SimpleJUnit5WithLegacyClientTest {

    private MongoCollection<Document> collection;
    private MongoClient client;
    private MongoServer server;

    @BeforeEach
    void setUp() {
        server = new MongoServer(new MemoryBackend());

        // bind on a random local port
        InetSocketAddress serverAddress = server.bind();

        client = new MongoClient(new ServerAddress(serverAddress));
        collection = client.getDatabase("testdb").getCollection("testcollection");
    }

    @AfterEach
    void tearDown() {
        client.close();
        server.shutdown();
    }

    @Test
    void testSimpleInsertQuery() throws Exception {
        assertThat(collection.countDocuments()).isZero();

        // creates the database and collection in memory and insert the object
        Document obj = new Document("_id", 1).append("key", "value");
        collection.insertOne(obj);

        assertThat(collection.countDocuments()).isEqualTo(1L);
        assertThat(collection.find().first()).isEqualTo(obj);
    }

}
<file_sep>/build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencyLocking {
        lockAllConfigurations()
    }
}

plugins {
    id 'com.github.johnrengelman.shadow' version 'latest.release'
    id 'org.sonarqube' version 'latest.release'
    id 'jacoco-report-aggregation'
}

allprojects {
    apply plugin: 'java-library'
    apply plugin: 'jacoco'

    version = '1.45.0-SNAPSHOT'

    group = 'de.bwaldvogel'

    sourceCompatibility = '11'

    compileJava.options.encoding = 'UTF-8'

    repositories {
        mavenCentral()
    }

    dependencies {
        components.all { ComponentMetadataDetails details ->
            if (details.id.version =~ /(?i).+(-|\.)(CANDIDATE|RC|BETA|ALPHA|PR|M\d+).*/) {
                details.status = 'milestone'
            }
        }
    }

    dependencyLocking {
        lockAllConfigurations()
    }
}

subprojects {
    dependencies {
        implementation group: 'org.slf4j', name: 'slf4j-api', version: 'latest.release'

        testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: 'latest.release'
        testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: 'latest.release'
        testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: 'latest.release'
        testImplementation group: 'org.assertj', name: 'assertj-core', version: 'latest.release'
        testRuntimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: '[1.3.0, 1.4.0)'
        testRuntimeOnly group: 'org.slf4j', name: 'jcl-over-slf4j', version: 'latest.release'
    }

    test {
        useJUnitPlatform()

        maxHeapSize = "256m"
        systemProperties['io.netty.leakDetectionLevel'] = 'advanced'
    }
}

ext {
    title = 'mongo-java-server'
    isReleaseVersion = !version.endsWith('SNAPSHOT')
}

jar {
    manifest {
        attributes 'Implementation-Title': title, 'Implementation-Version': archiveVersion
    }
}

allprojects {
    task sourceJar(type: Jar) {
        archiveClassifier = "sources"
        from sourceSets.main.allJava
    }

    task javadocJar(type: Jar, dependsOn: javadoc) {
        archiveClassifier = "javadoc"
        from javadoc.destinationDir
    }
}

configure(allprojects.findAll {it.name != 'mongo-java-server-examples'}) {
    apply plugin: 'maven-publish'
    apply plugin: 'signing'

    publishing {
        publications {
            mavenJava(MavenPublication) {
                groupId = 'de.bwaldvogel'
                artifactId = project.name
                version = project.version
                pom {
                    name = title
                    description = 'Fake implementation of MongoDB in Java that speaks the wire protocol'
                    url = 'https://github.com/bwaldvogel/mongo-java-server'
                    inceptionYear = '2012'

                    licenses {
                        license {
                            name = 'The BSD License'
                            url = 'http://www.opensource.org/licenses/bsd-license.php'
                            distribution = 'repo'
                        }
                    }
                    developers {
                        developer {
                            id = 'bwaldvogel'
                            name = '<NAME>'
                            email = '<EMAIL>'
                        }
                    }
                    scm {
                        url = '<EMAIL>:bwaldvogel/mongo-java-server.git'
                        connection = 'scm:git:git@github.com:bwaldvogel/mongo-java-server.git'
                        developerConnection = 'scm:git:git@github.com:bwaldvogel/mongo-java-server.git'
                    }
                }

                from components.java

                artifact sourceJar
                artifact javadocJar

                versionMapping {
                    usage('java-api') {
                        fromResolutionOf('runtimeClasspath')
                    }
                    usage('java-runtime') {
                        fromResolutionResult()
                    }
                }
            }
        }
        repositories {
            maven {
                url isReleaseVersion ? 'https://oss.sonatype.org/service/local/staging/deploy/maven2' : 'https://oss.sonatype.org/content/repositories/snapshots/'
                credentials {
                    username = project.hasProperty('nexusUsername') ? project.property('nexusUsername') : System.getenv('NEXUS_USERNAME')
                    password = project.hasProperty('nexusPassword') ? project.property('nexusPassword') : System.getenv('NEXUS_PASSWORD')
                }
            }
        }
    }

    signing {
        useGpgCmd()
        sign publishing.publications.mavenJava
    }

    tasks.withType(Sign) {
        onlyIf { isReleaseVersion }
    }

}

wrapper {
    gradleVersion = "8.1.1"
    distributionType = Wrapper.DistributionType.ALL
}

javadoc {
    options.addBooleanOption('html5', true)
}

dependencies {
    api project(':mongo-java-server-core')
    api project(':mongo-java-server-memory-backend')

    jacocoAggregation subprojects
}

tasks.named('check') {
    dependsOn tasks.named('testCodeCoverageReport', JacocoReport)
}

<file_sep>/core/src/main/java/de/bwaldvogel/mongo/oplog/OplogDocumentFields.java
package de.bwaldvogel.mongo.oplog;

import de.bwaldvogel.mongo.backend.Constants;

public interface OplogDocumentFields {
    String ID = Constants.ID_FIELD;
    String TIMESTAMP = "ts";
    String O = "o";
    String O2 = "o2";
    String ID_DATA_KEY = "_data";
    String NAMESPACE = "ns";
    String OPERATION_TYPE = "op";
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/NoSuchCommandException.java
package de.bwaldvogel.mongo.exception;

public class NoSuchCommandException extends MongoServerError {

    private static final long serialVersionUID = 1L;

    public NoSuchCommandException(String command) {
        super(ErrorCode.CommandNotFound, "no such command: '" + command + "'");
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/wire/message/MongoReply.java
package de.bwaldvogel.mongo.wire.message;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.wire.ReplyFlag;

public class MongoReply {
    private final MessageHeader header;
    private final List<? extends Document> documents;
    private final long cursorId;
    private int startingFrom;
    private int flags;

    public MongoReply(MessageHeader header, Document document, ReplyFlag... replyFlags) {
        this(header, List.of(document), 0, replyFlags);
    }

    public MongoReply(MessageHeader header, Iterable<? extends Document> documents, long cursorId, ReplyFlag... replyFlags) {
        this.cursorId = cursorId;
        this.header = header;
        this.documents = StreamSupport.stream(documents.spliterator(), false).collect(Collectors.toList());
        for (ReplyFlag replyFlag : replyFlags) {
            flags = replyFlag.addTo(flags);
        }
    }

    public MessageHeader getHeader() {
        return header;
    }

    public List<Document> getDocuments() {
        return Collections.unmodifiableList(documents);
    }

    public long getCursorId() {
        return cursorId;
    }

    public int getStartingFrom() {
        return startingFrom;
    }

    public int getFlags() {
        return flags;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getSimpleName());
        sb.append("(");
        sb.append("documents: ").append(getDocuments());
        sb.append(")");
        return sb.toString();
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/MongoSilentServerException.java
package de.bwaldvogel.mongo.exception;

/**
 * similar to {@link MongoServerException} but will not be logged as error with stacktrace
 */
public class MongoSilentServerException extends MongoServerException {

    private static final long serialVersionUID = 1L;

    public MongoSilentServerException(String message) {
        super(message);
        setLogError(false);
    }

}
<file_sep>/h2-backend/src/test/java/de/bwaldvogel/mongo/backend/memory/H2OnDiskBackendTest.java
package de.bwaldvogel.mongo.backend.memory;

import static de.bwaldvogel.mongo.backend.TestUtils.json;
import static de.bwaldvogel.mongo.backend.TestUtils.toArray;

import java.nio.file.Path;
import java.util.List;

import org.bson.Document;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mongodb.client.model.IndexOptions;

import de.bwaldvogel.mongo.MongoBackend;
import de.bwaldvogel.mongo.backend.AbstractBackendTest;
import de.bwaldvogel.mongo.backend.h2.H2Backend;

class H2OnDiskBackendTest extends AbstractBackendTest {

    private static final Logger log = LoggerFactory.getLogger(H2OnDiskBackendTest.class);

    private static H2Backend backend;

    @TempDir
    static Path tempFolder;

    private static Path tempFile;

    @Override
    protected void setUpBackend() throws Exception {
        if (tempFile == null) {
            tempFile = tempFolder.resolve(getClass().getSimpleName() + ".mv");
            log.debug("created {} for testing", tempFile);
        }
        super.setUpBackend();
    }

    @Override
    protected MongoBackend createBackend() throws Exception {
        backend = new H2Backend(tempFile.toString(), clock);
        backend.getMvStore().setAutoCommitDelay(0);
        return backend;
    }

    @Test
    void testShutdownAndRestart() throws Exception {
        collection.insertOne(json("_id: 1"));
        collection.insertOne(json("_id: 2"));

        long versionBeforeUpdate = backend.getMvStore().commit();

        collection.findOneAndUpdate(json("_id: 2"), json("$set: {x: 10}"));

        long versionAfterUpdate = backend.getMvStore().commit();
        assertThat(versionAfterUpdate).isEqualTo(versionBeforeUpdate + 1);

        restart();

        assertThat(collection.find())
            .containsExactly(json("_id: 1"), json("_id: 2, x: 10"));
    }

    @Test
    void testShutdownAndRestartOpensDatabasesAndCollections() throws Exception {
        List<String> dbs = List.of("local", "testdb1", "testdb2");
        for (String db : dbs) {
            for (String coll : new String[] { "collection1", "collection2" }) {
                syncClient.getDatabase(db).getCollection(coll).insertOne(json(""));
            }
        }
        assertThat(syncClient.listDatabaseNames())
            .isEqualTo(dbs);

        restart();

        assertThat(syncClient.listDatabaseNames())
            .isEqualTo(dbs);
    }

    @Test
    void testShutdownAndRestartOpensIndexes() throws Exception {
        collection.createIndex(json("a: 1"));
        collection.createIndex(json("b: 1"));
        assertThat(collection.listIndexes()).hasSize(3);

        collection.insertOne(json("_id: 1"));
        collection.insertOne(json("_id: 2"));

        List<String> databaseNames = toArray(syncClient.listDatabaseNames());

        restart();

        assertThat(syncClient.listDatabaseNames()).containsExactlyElementsOf(databaseNames);
        assertThat(collection.countDocuments()).isEqualTo(2);

        assertThat(collection.listIndexes()).containsExactlyElementsOf(toArray(collection.listIndexes()));
    }

    @Test
    void testShutdownAndRestartKeepsStatistics() throws Exception {
        collection.createIndex(json("a: 1"), new IndexOptions().unique(true));
        collection.createIndex(json("b: 1"), new IndexOptions().unique(true));

        collection.insertOne(json("_id: 1, a: 10, b: 100"));
        collection.insertOne(json("_id: 2, a: 20, b: 200"));

        backend.commit();

        Document statsBefore = db.runCommand(json("dbStats: 1, scale: 1"));

        restart();

        Document statsAfter = db.runCommand(json("dbStats: 1, scale: 1"));

        assertThat(statsAfter).isEqualTo(statsBefore);
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/bson/Json.java
package de.bwaldvogel.mongo.bson;

import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Collection;
import java.util.UUID;
import java.util.stream.Collectors;

import de.bwaldvogel.mongo.backend.Missing;

public final class Json {

    private Json() {
    }

    public static String toJsonValue(Object value) {
        return toJsonValue(value, false, "{", "}");
    }

    public static String toCompactJsonValue(Object value) {
        return toJsonValue(value, true, "{ ", " }");
    }

    public static String toJsonValue(Object value, boolean compactKey, String jsonPrefix, String jsonSuffix) {
        if (Missing.isNullOrMissing(value)) {
            return "null";
        }
        if (value instanceof Number) {
            return value.toString();
        }
        if (value instanceof Boolean) {
            return value.toString();
        }
        if (value instanceof String) {
            return "\"" + escapeJson((String) value) + "\"";
        }
        if (value instanceof Document) {
            Document document = (Document) value;
            return document.toString(compactKey, jsonPrefix, jsonSuffix);
        }
        if (value instanceof Instant) {
            Instant instant = (Instant) value;
            return toJsonValue(instant.toString());
        }
        if (value instanceof Collection) {
            Collection<?> collection = (Collection<?>) value;
            if (collection.isEmpty()) {
                return "[]";
            }
            return collection.stream()
                .map(v -> toJsonValue(v, compactKey, "{ ", " }"))
                .collect(Collectors.joining(", ", "[ ", " ]"));
        }
        if (value instanceof ObjectId) {
            ObjectId objectId = (ObjectId) value;
            return objectId.getHexData();
        }
        if (value instanceof BinData) {
            BinData binData = (BinData) value;
            return "BinData(0, " + toHex(binData.getData()) + ")";
        }
        if (value instanceof LegacyUUID) {
            UUID uuid = ((LegacyUUID) value).getUuid();
            return "BinData(3, " + toHex(uuid) + ")";
        }
        if (value instanceof UUID) {
            UUID uuid = (UUID) value;
            return "UUID(\"" + uuid + "\")";
        }
        return toJsonValue(value.toString());
    }

    static String escapeJson(String input) {
        String escaped = input;
        escaped = escaped.replace("\\", "\\\\");
        escaped = escaped.replace("\"", "\\\"");
        escaped = escaped.replace("\b", "\\b");
        escaped = escaped.replace("\f", "\\f");
        escaped = escaped.replace("\n", "\\n");
        escaped = escaped.replace("\r", "\\r");
        escaped = escaped.replace("\t", "\\t");
        return escaped;
    }

    private static StringBuilder toHex(UUID uuid) {
        byte[] bytes = toBytes(uuid);
        StringBuilder hex = new StringBuilder();
        for (int i = bytes.length; i > 0; i--) {
            hex.append(String.format("%02X", bytes[i - 1]));
        }
        return hex;
    }

    private static StringBuilder toHex(byte[] bytes) {
        StringBuilder hex = new StringBuilder();
        for (byte aByte : bytes) {
            hex.append(String.format("%02X", aByte));
        }
        return hex;
    }

    private static byte[] toBytes(UUID uuid) {
        ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]);
        byteBuffer.putLong(uuid.getLeastSignificantBits());
        byteBuffer.putLong(uuid.getMostSignificantBits());
        return byteBuffer.array();
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/stage/FacetStage.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import de.bwaldvogel.mongo.MongoCollection;
import de.bwaldvogel.mongo.MongoDatabase;
import de.bwaldvogel.mongo.backend.DatabaseResolver;
import de.bwaldvogel.mongo.backend.aggregation.Aggregation;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.oplog.Oplog;

public class FacetStage implements AggregationStage {

    private final Map<String, Aggregation> facets = new LinkedHashMap<>();

    public FacetStage(Document facetsConfiguration, DatabaseResolver databaseResolver, MongoDatabase database,
                      MongoCollection<?> collection, Oplog oplog) {
        for (Entry<String, Object> entry : facetsConfiguration.entrySet()) {
            Aggregation aggregation = Aggregation.fromPipeline(entry.getValue(), databaseResolver, database, collection, oplog);
            facets.put(entry.getKey(), aggregation);
        }
    }

    @Override
    public String name() {
        return "$facet";
    }

    @Override
    public Stream<Document> apply(Stream<Document> stream) {
        List<Document> allDocuments = stream.collect(Collectors.toList());
        Document result = new Document();
        for (Entry<String, Aggregation> entry : facets.entrySet()) {
            Aggregation aggregation = entry.getValue();
            List<Document> documents = aggregation.runStages(allDocuments.stream());
            result.put(entry.getKey(), documents);
        }
        return Stream.of(result);
    }

    @Override
    public boolean isModifying() {
        return facets.values().stream().anyMatch(Aggregation::isModifying);
    }
}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/StreamUtilsTest.java
package de.bwaldvogel.mongo.backend;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.entry;

import java.util.AbstractMap.SimpleEntry;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;

public class StreamUtilsTest {

    @Test
    void testToLinkedHashMap() throws Exception {
        Map<String, String> result = Stream.of("a", "b", "c")
            .map(value -> new SimpleEntry<>(value, value))
            .collect(StreamUtils.toLinkedHashMap());
        assertThat(result)
            .containsExactly(
                entry("a", "a"),
                entry("b", "b"),
                entry("c", "c")
            );
    }

    @Test
    void testToLinkedHashMap_duplicates() throws Exception {
        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Stream.of("a", "b", "c", "b")
                .map(value -> new SimpleEntry<>(value, value))
                .collect(StreamUtils.toLinkedHashMap()))
            .withMessage("Duplicate key 'b'");
    }

    @Test
    void testToLinkedHashSet() throws Exception {
        Set<String> result = Stream.of("a", "b", "c", "b")
            .collect(StreamUtils.toLinkedHashSet());
        assertThat(result).containsExactly("a", "b", "c");
    }

}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/aggregation/stage/UnwindStageTest.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import static de.bwaldvogel.mongo.TestUtils.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;

import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.MongoServerError;

public class UnwindStageTest {

    @Test
    void testUnwind() throws Exception {
        assertThat(unwind("$field",
            json("_id: 1, field: [1, 2, 3]"),
            json("_id: 2, field: [2, 3]"),
            json("_id: 3")))
            .containsExactly(
                json("_id: 1, field: 1"),
                json("_id: 1, field: 2"),
                json("_id: 1, field: 3"),
                json("_id: 2, field: 2"),
                json("_id: 2, field: 3")
            );

        assertThat(unwind(json("path: '$field'"), json("_id: 1, field: ['A', 'B']")))
            .containsExactly(
                json("_id: 1, field: 'A'"),
                json("_id: 1, field: 'B'")
            );
    }

    @Test
    void testUnwindWithSubdocument() throws Exception {
        assertThat(unwind("$field.values",
            json("_id: 1, field: {values: [1, 2, 3]}"),
            json("_id: 2, field: {values: [2, 3]}")))
            .containsExactly(
                json("_id: 1, field: {values: 1}"),
                json("_id: 1, field: {values: 2}"),
                json("_id: 1, field: {values: 3}"),
                json("_id: 2, field: {values: 2}"),
                json("_id: 2, field: {values: 3}")
            );

        assertThat(unwind("$field.values",
            json("_id: 1, field: {x: 1, values: [1, 2, 3]}"),
            json("_id: 2, field: {x: 2, values: [2, 3]}")))
            .containsExactly(
                json("_id: 1, field: {x: 1, values: 1}"),
                json("_id: 1, field: {x: 1, values: 2}"),
                json("_id: 1, field: {x: 1, values: 3}"),
                json("_id: 2, field: {x: 2, values: 2}"),
                json("_id: 2, field: {x: 2, values: 3}")
            );
    }

    private List<Document> unwind(Object input, Document... documents) {
        return new UnwindStage(input).apply(Stream.of(documents)).collect(Collectors.toList());
    }

    @Test
    void testIllegalParameter() throws Exception {
        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> new UnwindStage("illegalField"))
            .withMessage("[Error 28818] path option to $unwind stage should be prefixed with a '$': illegalField");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> new UnwindStage(null))
            .withMessage("[Error 15981] expected either a string or an object as specification for $unwind stage, got null");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> new UnwindStage(json("")))
            .withMessage("[Error 28812] no path specified to $unwind stage");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> new UnwindStage(json("path: {}")))
            .withMessage("[Error 28808] expected a string as the path for $unwind stage, got object");
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/QueryFilter.java
package de.bwaldvogel.mongo.backend;

import java.util.HashMap;
import java.util.Map;

enum QueryFilter {

    AND("$and"),
    OR("$or"),
    NOR("$nor"),
    EXPR("$expr"),
    ;

    private final String value;

    QueryFilter(String value) {
        this.value = value;
    }

    private static final Map<String, QueryFilter> MAP = new HashMap<>();

    static {
        for (QueryFilter filter : QueryFilter.values()) {
            QueryFilter old = MAP.put(filter.getValue(), filter);
            Assert.isNull(old, () -> "Duplicate value: " + filter.getValue());
        }
    }

    public String getValue() {
        return value;
    }

    static boolean isQueryFilter(String value) {
        return MAP.containsKey(value);
    }

    static QueryFilter fromValue(String value) throws IllegalArgumentException {
        QueryFilter filter = MAP.get(value);
        Assert.notNull(filter, () -> "Illegal filter: " + value);
        return filter;
    }

    @Override
    public String toString() {
        return value;
    }
}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/aggregation/stage/ReplaceRootStageTest.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import static de.bwaldvogel.mongo.TestUtils.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import org.junit.jupiter.api.Test;

import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.MongoServerError;

public class ReplaceRootStageTest {

    @Test
    void testReplaceRoot() throws Exception {
        assertThat(replaceRoot(json("newRoot: '$a'"), json("a: { b: { c: 1 } }"))).isEqualTo(json("b: { c: 1 }"));
        assertThat(replaceRoot(json("newRoot: '$a.b'"), json("a: { b: { c: 1 } }"))).isEqualTo(json("c: 1"));
        assertThat(replaceRoot(json("newRoot: { x: '$a.b' }"), json("a: { b: { c: 1 } }"))).isEqualTo(json("x: { c: 1 }"));
    }

    private static Document replaceRoot(Document replaceRoot, Document document) {
        return new ReplaceRootStage(replaceRoot).replaceRoot(document);
    }

    @Test
    void testIllegalReplaceRoot() throws Exception {
        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> new ReplaceRootStage(json("")))
            .withMessage("[Error 40231] no newRoot specified for the $replaceRoot stage");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> new ReplaceRootStage(json("newRoot: 1")).replaceRoot(json("a: { b: {} }")))
            .withMessage("[Error 40228] 'newRoot' expression must evaluate to an object, but resulting value was: 1. Type of resulting value: 'int'. Input document: {a: {b: {}}}");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> new ReplaceRootStage(json("newRoot: 'x'")).replaceRoot(json("a: { b: {} }")))
            .withMessage("[Error 40228] 'newRoot' expression must evaluate to an object, but resulting value was: \"x\". Type of resulting value: 'string'. Input document: {a: {b: {}}}");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> new ReplaceRootStage(json("newRoot: '$c'")).replaceRoot(json("a: { b: {} }")))
            .withMessage("[Error 40228] 'newRoot' expression must evaluate to an object, but resulting value was: null. Type of resulting value: 'missing'. Input document: {a: {b: {}}}");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> new ReplaceRootStage(json("newRoot: '$a.c'")).replaceRoot(json("a: { b: {} }")))
            .withMessage("[Error 40228] 'newRoot' expression must evaluate to an object, but resulting value was: null. Type of resulting value: 'missing'. Input document: {a: {b: {}}}");
    }

}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/aggregation/accumulator/MaxAccumulatorTest.java
package de.bwaldvogel.mongo.backend.aggregation.accumulator;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

import org.junit.jupiter.api.Test;

public class MaxAccumulatorTest {

    private final ComparingAccumulator accumulator = new MaxAccumulator(null, null);

    @Test
    void testAccumulateNumbers() throws Exception {
        accumulator.aggregate(1);
        accumulator.aggregate(5);
        accumulator.aggregate(3);

        Object result = accumulator.getResult();
        assertThat(result).isEqualTo(5);
    }

    @Test
    void testAccumulateArrays() throws Exception {
        accumulator.aggregate(List.of(10, 20, 30));
        accumulator.aggregate(List.of(3, 40));
        accumulator.aggregate(List.of(11, 25));

        Object result = accumulator.getResult();
        assertThat(result).isEqualTo(List.of(11, 25));
    }

    @Test
    void testAccumulateArraysAndNonArray() throws Exception {
        accumulator.aggregate(List.of(3, 40));
        accumulator.aggregate(List.of(10, 20, 30));
        accumulator.aggregate(50);

        Object result = accumulator.getResult();
        assertThat(result).isEqualTo(List.of(10, 20, 30));
    }

    @Test
    void testAccumulateNothing() throws Exception {
        Object result = accumulator.getResult();
        assertThat(result).isNull();
    }

}
<file_sep>/core/build.gradle
dependencies {
    implementation group: 'io.netty', name: 'netty-transport', version: 'latest.release'
    implementation group: 'io.netty', name: 'netty-codec', version: 'latest.release'
    implementation group: 'io.netty', name: 'netty-handler', version: 'latest.release'

    testImplementation group: 'org.mongodb', name: 'mongo-java-driver', version: 'latest.release'
    testImplementation "org.mockito:mockito-core:latest.release"
    testImplementation "org.mockito:mockito-junit-jupiter:latest.release"
    testImplementation "nl.jqno.equalsverifier:equalsverifier:latest.release"
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/InvalidNamespaceError.java
package de.bwaldvogel.mongo.exception;

public class InvalidNamespaceError extends MongoServerError {

    private static final long serialVersionUID = 1L;

    public InvalidNamespaceError(String namespace) {
        super(ErrorCode.InvalidNamespace, "Invalid system namespace: " + namespace);
    }

}
<file_sep>/README.md
[![CI](https://github.com/bwaldvogel/mongo-java-server/workflows/CI/badge.svg)](https://github.com/bwaldvogel/mongo-java-server/actions)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/de.bwaldvogel/mongo-java-server/badge.svg)](http://maven-badges.herokuapp.com/maven-central/de.bwaldvogel/mongo-java-server)
[![codecov](https://codecov.io/gh/bwaldvogel/mongo-java-server/branch/main/graph/badge.svg?token=jZ7zBT4niu)](https://codecov.io/gh/bwaldvogel/mongo-java-server)
[![BSD 3-Clause License](https://img.shields.io/github/license/bwaldvogel/mongo-java-server.svg)](https://opensource.org/licenses/BSD-3-Clause)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/BenediktWaldvogel)

# MongoDB Java Server #

Fake implementation of the core [MongoDB][mongodb] server in Java that can be used for integration tests.

Think of H2/HSQLDB/SQLite but for MongoDB.

The [MongoDB Wire Protocol][wire-protocol] is implemented with [Netty][netty].
Different backends are possible and can be extended.

## In-Memory backend ##

The in-memory backend is the default backend that is typically used to fake MongoDB for integration tests.
It supports most CRUD operations, commands and the aggregation framework.
Some features are not yet implemented, such as transactions, full-text search or map/reduce.

Add the following Maven dependency to your project:

```xml
<dependency>
    <groupId>de.bwaldvogel</groupId>
    <artifactId>mongo-java-server</artifactId>
    <version>1.44.0</version>
</dependency>
```

### Example ###

```java
class SimpleTest {

    private MongoCollection<Document> collection;
    private MongoClient client;
    private MongoServer server;

    @BeforeEach
    void setUp() {
        server = new MongoServer(new MemoryBackend());

        // optionally:
        // server.enableSsl(key, keyPassword, certificate);
        // server.enableOplog();

        // bind on a random local port
        String connectionString = server.bindAndGetConnectionString();

        client = MongoClients.create(connectionString);
        collection = client.getDatabase("testdb").getCollection("testcollection");
    }

    @AfterEach
    void tearDown() {
        client.close();
        server.shutdown();
    }

    @Test
    void testSimpleInsertQuery() throws Exception {
        assertThat(collection.countDocuments()).isZero();

        // creates the database and collection in memory and insert the object
        Document obj = new Document("_id", 1).append("key", "value");
        collection.insertOne(obj);

        assertThat(collection.countDocuments()).isEqualTo(1L);
        assertThat(collection.find().first()).isEqualTo(obj);
    }

}
```

### Example with SpringBoot ###

```java
@RunWith(SpringRunner.class)
@SpringBootTest(classes={SimpleSpringBootTest.TestConfiguration.class})
public class SimpleSpringBootTest {

    @Autowired private MyRepository repository;

    @Before
    public void setUp() {
        // initialize your repository with some test data
        repository.deleteAll();
        repository.save(...);
    }

    @Test
    public void testMyRepository() {
        // test your repository ...
        ...
    }

    @Configuration
    @EnableMongoTestServer
    @EnableMongoRepositories(basePackageClasses={MyRepository.class})
    protected static class TestConfiguration {
        // test bean definitions ...
        ...
    }
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MongoTestServerConfiguration.class)
public @interface EnableMongoTestServer {

}

public class MongoTestServerConfiguration {
	@Bean
	public MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDbFactory) {
		return new MongoTemplate(mongoDbFactory);
	}

	@Bean
	public MongoDatabaseFactory mongoDbFactory(MongoServer mongoServer) {
		String connectionString = mongoServer.getConnectionString();
		return new SimpleMongoClientDatabaseFactory(connectionString + "/test");
	}

	@Bean(destroyMethod = "shutdown")
	public MongoServer mongoServer() {
		MongoServer mongoServer = new MongoServer(new MemoryBackend());
		mongoServer.bind();
		return mongoServer;
	}
}
```

## H2 MVStore backend ##

The [H2 MVStore][h2-mvstore] backend connects the server to a `MVStore` that
can either be in-memory or on-disk.

```xml
<dependency>
    <groupId>de.bwaldvogel</groupId>
    <artifactId>mongo-java-server-h2-backend</artifactId>
    <version>1.44.0</version>
</dependency>
```

### Example ###

```java
public class Application {

    public static void main(String[] args) throws Exception {
        MongoServer server = new MongoServer(new H2Backend("database.mv"));
        server.bind("localhost", 27017);
    }

}
```

## PostgreSQL backend ##

The PostgreSQL backend is a proof-of-concept implementation that connects the server to a database in a running
PostgreSQL 9.5+ instance. Each MongoDB database is mapped to a schema in
Postgres and each MongoDB collection is stored as a table.

```xml
<dependency>
    <groupId>de.bwaldvogel</groupId>
    <artifactId>mongo-java-server-postgresql-backend</artifactId>
    <version>1.44.0</version>
</dependency>
```


### Example ###

```java
public class Application {

    public static void main(String[] args) throws Exception {
        DataSource dataSource = new org.postgresql.jdbc3.Jdbc3PoolingDataSource();
        dataSource.setDatabaseName(…);
        dataSource.setUser(…);
        dataSource.setPassword(…);
        MongoServer server = new MongoServer(new PostgresqlBackend(dataSource));
        server.bind("localhost", 27017);
    }

}
```
## Building a "fat" JAR that contains all dependencies ##

If you want to build a version that is not on Maven Central you can do the following:

1. Build a "fat" JAR that includes all dependencies using "`./gradlew shadowJar`"
2. Copy `build/libs/mongo-java-server-[version]-all.jar` to your project, e.g. to the `libs` directory.
3. Import that folder (e.g. via Gradle using `testCompile fileTree(dir: 'libs', include: '*.jar')`)

## Contributing ##

Please read the [contributing guidelines](CONTRIBUTING.md) if you want to contribute code to the project.

If you want to thank the author for this library or want to support the maintenance work, we are happy to receive a donation.

[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/BenediktWaldvogel)

## Ideas for other backends ##

### Faulty backend ###

A faulty backend could randomly fail queries or cause timeouts. This could be
used to test the client for error resilience.

### Fuzzy backend ###

Fuzzing the wire protocol could be used to check the robustness of client
drivers.

## Transactions ##

Please note that transactions are currently not supported.
Please see [the discussion in issue #143](https://github.com/bwaldvogel/mongo-java-server/issues/143).

When using `mongo-java-server` for integration tests, you can use [Testcontainers][testcontainers] or [Embedded MongoDB][embedded-mongodb] instead to spin-up a real MongoDB that will have full transaction support.

## Related Work ##

* [Testcontainers][testcontainers]
  * Can be used to spin-up a real MongoDB instance in a Docker container

* [Embedded MongoDB][embedded-mongodb]
  * Spins up a real MongoDB instance

* [fongo][fongo]
  * focus on unit testing
  * no wire protocol implementation
  * intercepts the java mongo driver
  * currently used in [nosql-unit][nosql-unit]

[mongodb]: http://www.mongodb.org/
[wire-protocol]: https://docs.mongodb.org/manual/reference/mongodb-wire-protocol/
[netty]: http://netty.io/
[testcontainers]: https://www.testcontainers.org/
[embedded-mongodb]: https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo
[fongo]: https://github.com/fakemongo/fongo
[nosql-unit]: https://github.com/lordofthejars/nosql-unit
[h2-mvstore]: http://www.h2database.com/html/mvstore.html
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/Range.java
package de.bwaldvogel.mongo.backend.aggregation;

public class Range {

    private final int start;
    private final int end;

    Range(int start, int end) {
        this.start = start;
        this.end = end;
    }

    public int getStart() {
        return start;
    }

    public int getEnd() {
        return end;
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/bson/BsonRegularExpression.java
package de.bwaldvogel.mongo.bson;

import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import de.bwaldvogel.mongo.backend.Assert;

public class BsonRegularExpression implements Bson {

    private static final long serialVersionUID = 1L;

    private static final String REGEX = "$regex";
    private static final String OPTIONS = "$options";

    private final String pattern;
    private final String options;

    private transient final AtomicReference<Pattern> compiledPattern = new AtomicReference<>(null);

    public BsonRegularExpression(String pattern, String options) {
        this.pattern = pattern;
        this.options = options;
    }

    public BsonRegularExpression(String pattern) {
        this(pattern, null);
    }

    public Document toDocument() {
        Document document = new Document(REGEX, pattern);
        if (options != null) {
            document.append(OPTIONS, options);
        }
        return document;
    }

    private static BsonRegularExpression fromDocument(Document queryObject) {
        String options = "";
        if (queryObject.containsKey(OPTIONS)) {
            options = queryObject.get(OPTIONS).toString();
        }

        String pattern = queryObject.get(REGEX).toString();
        return new BsonRegularExpression(pattern, options);
    }

    public static boolean isRegularExpression(Object object) {
        if (object instanceof Document) {
            return ((Document) object).containsKey(REGEX);
        } else {
            return object instanceof BsonRegularExpression;
        }
    }

    public static BsonRegularExpression convertToRegularExpression(Object pattern) {
        Assert.isTrue(isRegularExpression(pattern), () -> "'" + pattern + "' is not a regular expression");
        if (pattern instanceof BsonRegularExpression) {
            return (BsonRegularExpression) pattern;
        } else {
            return fromDocument((Document) pattern);
        }
    }

    public String getPattern() {
        return pattern;
    }

    public String getOptions() {
        return options;
    }

    private Pattern toPattern() {
        Pattern pattern = compiledPattern.get();
        if (pattern == null) {
            pattern = createPattern();
            compiledPattern.lazySet(pattern);
        }
        return pattern;
    }

    Pattern createPattern() {
        int flags = 0;
        for (char flag : options.toCharArray()) {
            switch (flag) {
                case 'i':
                    flags |= Pattern.CASE_INSENSITIVE;
                    break;
                case 'm':
                    flags |= Pattern.MULTILINE;
                    break;
                case 'x':
                    flags |= Pattern.COMMENTS;
                    break;
                case 's':
                    flags |= Pattern.DOTALL;
                    break;
                case 'u':
                    flags |= Pattern.UNICODE_CASE;
                    break;
                default:
                    throw new IllegalArgumentException("unknown pattern flag: '" + flag + "'");
            }

        }

        // always enable unicode aware case matching
        flags |= Pattern.UNICODE_CASE;

        return Pattern.compile(pattern, flags);
    }

    @Override
    public String toString() {
        Document representative = new Document("$regex", pattern);
        if (options != null) {
            representative.put("$options", options);
        }
        return Json.toJsonValue(representative);
    }

    public Matcher matcher(String string) {
        return toPattern().matcher(string);
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/AbstractMongoDatabase.java
package de.bwaldvogel.mongo.backend;

import static de.bwaldvogel.mongo.backend.Constants.ID_FIELD;
import static de.bwaldvogel.mongo.backend.Constants.PRIMARY_KEY_INDEX_NAME;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.bwaldvogel.mongo.MongoCollection;
import de.bwaldvogel.mongo.MongoDatabase;
import de.bwaldvogel.mongo.backend.aggregation.Aggregation;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.IndexNotFoundException;
import de.bwaldvogel.mongo.exception.InvalidNamespaceError;
import de.bwaldvogel.mongo.exception.MongoServerError;
import de.bwaldvogel.mongo.exception.MongoServerException;
import de.bwaldvogel.mongo.exception.MongoSilentServerException;
import de.bwaldvogel.mongo.exception.NamespaceExistsException;
import de.bwaldvogel.mongo.exception.NoSuchCommandException;
import de.bwaldvogel.mongo.oplog.NoopOplog;
import de.bwaldvogel.mongo.oplog.Oplog;
import de.bwaldvogel.mongo.wire.message.MongoQuery;
import io.netty.channel.Channel;

public abstract class AbstractMongoDatabase<P> implements MongoDatabase {

    private static final String NAMESPACES_COLLECTION_NAME = "system.namespaces";

    private static final String INDEXES_COLLECTION_NAME = "system.indexes";

    private static final Logger log = LoggerFactory.getLogger(AbstractMongoDatabase.class);

    protected final String databaseName;

    private final Map<String, MongoCollection<P>> collections = new ConcurrentHashMap<>();

    protected final AtomicReference<MongoCollection<P>> indexes = new AtomicReference<>();

    private final Map<Channel, List<Document>> lastResults = new ConcurrentHashMap<>();

    private MongoCollection<P> namespaces;

    protected final CursorRegistry cursorRegistry;

    protected AbstractMongoDatabase(String databaseName, CursorRegistry cursorRegistry) {
        this.databaseName = databaseName;
        this.cursorRegistry = cursorRegistry;
    }

    protected void initializeNamespacesAndIndexes() {
        this.namespaces = openOrCreateCollection(NAMESPACES_COLLECTION_NAME, CollectionOptions.withIdField("name"));
        this.collections.put(namespaces.getCollectionName(), namespaces);

        if (!namespaces.isEmpty()) {
            for (String name : listCollectionNamespaces()) {
                log.debug("opening {}", name);
                String collectionName = extractCollectionNameFromNamespace(name);
                MongoCollection<P> collection = openOrCreateCollection(collectionName, CollectionOptions.withDefaults());
                collections.put(collectionName, collection);
                log.debug("opened collection '{}'", collectionName);
            }

            MongoCollection<P> indexCollection = openOrCreateCollection(INDEXES_COLLECTION_NAME, CollectionOptions.withoutIdField());
            collections.put(indexCollection.getCollectionName(), indexCollection);
            this.indexes.set(indexCollection);
            for (Document indexDescription : indexCollection.queryAll()) {
                openOrCreateIndex(indexDescription);
            }
        }
    }

    @Override
    public final String getDatabaseName() {
        return databaseName;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "(" + getDatabaseName() + ")";
    }

    private Document commandError(Channel channel, String command, Document query) {
        // getlasterror must not clear the last error
        if (command.equalsIgnoreCase("getlasterror")) {
            return commandGetLastError(channel, command, query);
        } else if (command.equalsIgnoreCase("reseterror")) {
            return commandResetError(channel);
        }
        return null;
    }

    @Override
    public Document handleCommand(Channel channel, String command, Document query, DatabaseResolver databaseResolver, Oplog oplog) {
        Document commandErrorDocument = commandError(channel, command, query);
        if (commandErrorDocument != null) {
            return commandErrorDocument;
        }

        clearLastStatus(channel);

        if (command.equalsIgnoreCase("find")) {
            return commandFind(command, query);
        } else if (command.equalsIgnoreCase("insert")) {
            return commandInsert(channel, command, query, oplog);
        } else if (command.equalsIgnoreCase("update")) {
            return commandUpdate(channel, command, query, oplog);
        } else if (command.equalsIgnoreCase("delete")) {
            return commandDelete(channel, command, query, oplog);
        } else if (command.equalsIgnoreCase("create")) {
            return commandCreate(command, query);
        } else if (command.equalsIgnoreCase("createIndexes")) {
            String collectionName = (String) query.get(command);
            return commandCreateIndexes(query, collectionName);
        } else if (command.equalsIgnoreCase("count")) {
            return commandCount(command, query);
        } else if (command.equalsIgnoreCase("aggregate")) {
            return commandAggregate(command, query, databaseResolver, oplog);
        } else if (command.equalsIgnoreCase("distinct")) {
            MongoCollection<P> collection = resolveCollection(command, query);
            if (collection == null) {
                Document response = new Document("values", Collections.emptyList());
                Utils.markOkay(response);
                return response;
            } else {
                return collection.handleDistinct(query);
            }
        } else if (command.equalsIgnoreCase("drop")) {
            return commandDrop(query, oplog);
        } else if (command.equalsIgnoreCase("dropIndexes")) {
            return commandDropIndexes(query);
        } else if (command.equalsIgnoreCase("dbstats")) {
            return commandDatabaseStats();
        } else if (command.equalsIgnoreCase("collstats")) {
            MongoCollection<P> collection = resolveCollection(command, query);
            if (collection == null) {
                Document emptyStats = new Document()
                    .append("count", 0)
                    .append("size", 0);
                Utils.markOkay(emptyStats);
                return emptyStats;
            } else {
                return collection.getStats();
            }
        } else if (command.equalsIgnoreCase("validate")) {
            MongoCollection<P> collection = resolveCollection(command, query);
            if (collection == null) {
                String collectionName = query.get(command).toString();
                String fullCollectionName = getDatabaseName() + "." + collectionName;
                throw new MongoServerError(26, "NamespaceNotFound", "Collection '" + fullCollectionName + "' does not exist to validate.");
            }
            return collection.validate();
        } else if (command.equalsIgnoreCase("findAndModify")) {
            String collectionName = query.get(command).toString();
            MongoCollection<P> collection = resolveOrCreateCollection(collectionName);
            return collection.findAndModify(query);
        } else if (command.equalsIgnoreCase("listCollections")) {
            return listCollections();
        } else if (command.equalsIgnoreCase("listIndexes")) {
            String collectionName = query.get(command).toString();
            return listIndexes(collectionName);
        } else if (command.equals("triggerInternalException")) {
            throw new NullPointerException("For testing purposes");
        } else {
            log.error("unknown query: {}", query);
        }
        throw new NoSuchCommandException(command);
    }

    private Document listCollections() {
        List<Document> firstBatch = new ArrayList<>();
        for (String namespace : listCollectionNamespaces()) {
            if (namespace.endsWith(INDEXES_COLLECTION_NAME)) {
                continue;
            }
            Document collectionDescription = new Document();
            Document collectionOptions = new Document();
            String collectionName = extractCollectionNameFromNamespace(namespace);
            collectionDescription.put("name", collectionName);
            collectionDescription.put("options", collectionOptions);
            collectionDescription.put("info", new Document("readOnly", false));
            collectionDescription.put("type", "collection");
            collectionDescription.put("idIndex", getPrimaryKeyIndexDescription());
            firstBatch.add(collectionDescription);
        }

        return Utils.firstBatchCursorResponse(getDatabaseName() + ".$cmd.listCollections", firstBatch);
    }

    private static Document getPrimaryKeyIndexDescription() {
        return getPrimaryKeyIndexDescription(null);
    }

    private static Document getPrimaryKeyIndexDescription(String namespace) {
        Document indexDescription = new Document("key", new Document(ID_FIELD, 1))
            .append("name", PRIMARY_KEY_INDEX_NAME);

        if (namespace != null) {
            indexDescription.put("ns", namespace);
        }

        return indexDescription.append("v", 2);
    }

    private Iterable<String> listCollectionNamespaces() {
        return namespaces.queryAllAsStream()
            .map(document -> document.get("name").toString())
            ::iterator;
    }

    private Document listIndexes(String collectionName) {
        Stream<Document> indexes = Optional.ofNullable(resolveCollection(INDEXES_COLLECTION_NAME, false))
            .map(collection -> collection.handleQueryAsStream(new Document("ns", getFullCollectionNamespace(collectionName))))
            .orElse(Stream.empty())
            .map(indexDescription -> {
                Document clone = indexDescription.clone();
                clone.remove("ns");
                return clone;
            });
        return Utils.firstBatchCursorResponse(getDatabaseName() + ".$cmd.listIndexes", indexes);
    }

    protected MongoCollection<P> resolveOrCreateCollection(String collectionName) {
        final MongoCollection<P> collection = resolveCollection(collectionName, false);
        if (collection != null) {
            return collection;
        } else {
            return createCollection(collectionName, CollectionOptions.withDefaults());
        }
    }

    private Document commandFind(String command, Document query) {
        String collectionName = (String) query.get(command);
        MongoCollection<P> collection = resolveCollection(collectionName, false);
        if (collection == null) {
            return Utils.firstBatchCursorResponse(getFullCollectionNamespace(collectionName), Collections.emptyList());
        }
        QueryParameters queryParameters = toQueryParameters(query);
        QueryResult queryResult = collection.handleQuery(queryParameters);
        return toCursorResponse(collection, queryResult);
    }

    private static QueryParameters toQueryParameters(Document query) {
        int numberToSkip = ((Number) query.getOrDefault("skip", 0)).intValue();
        int numberToReturn = ((Number) query.getOrDefault("limit", 0)).intValue();
        int batchSize = ((Number) query.getOrDefault("batchSize", 0)).intValue();

        Document querySelector = new Document();
        querySelector.put("$query", query.getOrDefault("filter", new Document()));
        querySelector.put("$orderby", query.get("sort"));

        Document projection = (Document) query.get("projection");
        return new QueryParameters(querySelector, numberToSkip, numberToReturn, batchSize, projection);
    }

    private QueryParameters toQueryParameters(MongoQuery query, int numberToSkip, int batchSize) {
        return new QueryParameters(query.getQuery(), numberToSkip, 0, batchSize, query.getReturnFieldSelector());
    }

    private Document toCursorResponse(MongoCollection<P> collection, QueryResult queryResult) {
        List<Document> documents = new ArrayList<>();
        for (Document document : queryResult) {
            documents.add(document);
        }
        return Utils.firstBatchCursorResponse(collection.getFullName(), documents, queryResult.getCursorId());
    }

    private Document commandInsert(Channel channel, String command, Document query, Oplog oplog) {
        String collectionName = query.get(command).toString();
        boolean isOrdered = Utils.isTrue(query.get("ordered"));
        log.trace("ordered: {}", isOrdered);

        @SuppressWarnings("unchecked")
        List<Document> documents = (List<Document>) query.get("documents");

        List<Document> writeErrors = insertDocuments(channel, collectionName, documents, oplog, isOrdered);
        Document result = new Document();
        result.put("n", Integer.valueOf(documents.size()));
        if (!writeErrors.isEmpty()) {
            result.put("writeErrors", writeErrors);
        }
        // odd by true: also mark error as okay
        Utils.markOkay(result);
        return result;
    }

    private Document commandUpdate(Channel channel, String command, Document query, Oplog oplog) {
        clearLastStatus(channel);
        String collectionName = query.get(command).toString();
        boolean isOrdered = Utils.isTrue(query.get("ordered"));
        log.trace("ordered: {}", isOrdered);

        @SuppressWarnings("unchecked")
        List<Document> updates = (List<Document>) query.get("updates");
        int nMatched = 0;
        int nModified = 0;
        Collection<Document> upserts = new ArrayList<>();

        List<Document> writeErrors = new ArrayList<>();

        Document response = new Document();
        for (int i = 0; i < updates.size(); i++) {
            Document updateObj = updates.get(i);
            Document selector = (Document) updateObj.get("q");
            Document update = (Document) updateObj.get("u");
            ArrayFilters arrayFilters = ArrayFilters.parse(updateObj, update);
            boolean multi = Utils.isTrue(updateObj.get("multi"));
            boolean upsert = Utils.isTrue(updateObj.get("upsert"));
            final Document result;
            try {
                result = updateDocuments(collectionName, selector, update, arrayFilters, multi, upsert, oplog);
            } catch (MongoServerException e) {
                writeErrors.add(toWriteError(i, e));
                continue;
            }
            if (result.containsKey("upserted")) {
                final Object id = result.get("upserted");
                final Document upserted = new Document("index", i);
                upserted.put(ID_FIELD, id);
                upserts.add(upserted);
            }
            nMatched += ((Integer) result.get("n")).intValue();
            nModified += ((Integer) result.get("nModified")).intValue();
        }

        response.put("n", nMatched + upserts.size());
        response.put("nModified", nModified);
        if (!upserts.isEmpty()) {
            response.put("upserted", upserts);
        }
        if (!writeErrors.isEmpty()) {
            response.put("writeErrors", writeErrors);
        }
        Utils.markOkay(response);
        putLastResult(channel, response);
        return response;
    }

    private Document commandDelete(Channel channel, String command, Document query, Oplog oplog) {
        String collectionName = query.get(command).toString();
        boolean isOrdered = Utils.isTrue(query.get("ordered"));
        log.trace("ordered: {}", isOrdered);

        @SuppressWarnings("unchecked")
        List<Document> deletes = (List<Document>) query.get("deletes");
        int n = 0;
        for (Document delete : deletes) {
            final Document selector = (Document) delete.get("q");
            final int limit = ((Number) delete.get("limit")).intValue();
            Document result = deleteDocuments(channel, collectionName, selector, limit, oplog);
            Integer resultNumber = (Integer) result.get("n");
            n += resultNumber.intValue();
        }

        Document response = new Document("n", Integer.valueOf(n));
        Utils.markOkay(response);
        return response;
    }

    private Document commandCreate(String command, Document query) {
        String collectionName = query.get(command).toString();

        CollectionOptions collectionOptions = CollectionOptions.fromQuery(query);
        collectionOptions.validate();
        createCollectionOrThrowIfExists(collectionName, collectionOptions);

        Document response = new Document();
        Utils.markOkay(response);
        return response;
    }

    @Override
    public MongoCollection<P> createCollectionOrThrowIfExists(String collectionName, CollectionOptions options) {
        MongoCollection<P> collection = resolveCollection(collectionName, false);
        if (collection != null) {
            throw new NamespaceExistsException("Collection already exists. NS: " + collection.getFullName());
        }

        return createCollection(collectionName, options);
    }

    private Document commandCreateIndexes(Document query, String collectionName) {
        int indexesBefore = countIndexes();

        @SuppressWarnings("unchecked")
        Collection<Document> indexDescriptions = (Collection<Document>) query.get("indexes");
        for (Document indexDescription : indexDescriptions) {
            indexDescription.putIfAbsent("ns", getFullCollectionNamespace(collectionName));
            addIndex(indexDescription);
        }

        int indexesAfter = countIndexes();

        Document response = new Document();
        response.put("numIndexesBefore", Integer.valueOf(indexesBefore));
        response.put("numIndexesAfter", Integer.valueOf(indexesAfter));
        Utils.markOkay(response);
        return response;
    }

    private Document commandDropIndexes(Document query) {
        String collectionName = (String) query.get("dropIndexes");
        MongoCollection<P> collection = resolveCollection(collectionName, false);
        if (collection != null) {
            dropIndexes(collection, query);
        }
        Document response = new Document();
        Utils.markOkay(response);
        return response;
    }

    private void dropIndexes(MongoCollection<P> collection, Document query) {
        Object index = query.get("index");
        Assert.notNull(index, () -> "Index name must not be null");
        MongoCollection<P> indexCollection = indexes.get();
        if (Objects.equals(index, "*")) {
            for (Document indexDocument : indexCollection.queryAll()) {
                Document indexKeys = (Document) indexDocument.get("key");
                if (!isPrimaryKeyIndex(indexKeys)) {
                    dropIndex(collection, indexDocument);
                }
            }
        } else if (index instanceof String) {
            dropIndex(collection, new Document("name", index));
        } else {
            Document indexKeys = (Document) index;
            Document indexQuery = new Document("key", indexKeys).append("ns", collection.getFullName());
            Document indexToDrop = CollectionUtils.getSingleElement(indexCollection.handleQuery(indexQuery),
                () -> new IndexNotFoundException(indexKeys));
            int numDeleted = dropIndex(collection, indexToDrop);
            Assert.equals(numDeleted, 1, () -> "Expected one deleted document");
        }
    }

    private int dropIndex(MongoCollection<P> collection, Document indexDescription) {
        String indexName = (String) indexDescription.get("name");
        dropIndex(collection, indexName);
        return indexes.get().deleteDocuments(indexDescription, -1);
    }

    protected void dropIndex(MongoCollection<P> collection, String indexName) {
        collection.dropIndex(indexName);
    }

    protected int countIndexes() {
        MongoCollection<P> indexesCollection = indexes.get();
        if (indexesCollection == null) {
            return 0;
        } else {
            return indexesCollection.count();
        }
    }

    private Collection<MongoCollection<P>> collections() {
        return collections.values().stream()
            .filter(collection -> !isSystemCollection(collection.getCollectionName()))
            .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    private Document commandDatabaseStats() {
        Document response = new Document("db", getDatabaseName());
        response.put("collections", Integer.valueOf(collections().size()));

        long storageSize = getStorageSize();
        long fileSize = getFileSize();
        long indexSize = 0;
        int objects = 0;
        double dataSize = 0;
        double averageObjectSize = 0;

        for (MongoCollection<P> collection : collections()) {
            Document stats = collection.getStats();
            objects += ((Number) stats.get("count")).intValue();
            dataSize += ((Number) stats.get("size")).doubleValue();

            Document indexSizes = (Document) stats.get("indexSize");
            for (String indexName : indexSizes.keySet()) {
                indexSize += ((Number) indexSizes.get(indexName)).longValue();
            }

        }
        if (objects > 0) {
            averageObjectSize = dataSize / ((double) objects);
        }
        response.put("objects", Integer.valueOf(objects));
        response.put("avgObjSize", Double.valueOf(averageObjectSize));
        if (dataSize == 0.0) {
            response.put("dataSize", Integer.valueOf(0));
        } else {
            response.put("dataSize", Double.valueOf(dataSize));
        }
        response.put("storageSize", Long.valueOf(storageSize));
        response.put("numExtents", Integer.valueOf(0));
        response.put("indexes", Integer.valueOf(countIndexes()));
        response.put("indexSize", Long.valueOf(indexSize));
        response.put("fileSize", Long.valueOf(fileSize));
        response.put("nsSizeMB", Integer.valueOf(0));
        Utils.markOkay(response);
        return response;
    }

    protected abstract long getFileSize();

    protected abstract long getStorageSize();

    private Document commandDrop(Document query, Oplog oplog) {
        String collectionName = query.get("drop").toString();

        MongoCollection<P> collection = resolveCollection(collectionName, false);
        if (collection == null) {
            throw new MongoSilentServerException("ns not found");
        }

        int numIndexes = collection.getNumIndexes();
        dropCollection(collectionName, oplog);
        Document response = new Document();
        response.put("nIndexesWas", Integer.valueOf(numIndexes));
        response.put("ns", collection.getFullName());
        Utils.markOkay(response);
        return response;
    }

    private Document commandGetLastError(Channel channel, String command, Document query) {
        query.forEach((subCommand, value) -> {
            if (subCommand.equals(command)) {
                return;
            }

            switch (subCommand) {
                case "w":
                    // ignore
                    break;
                case "fsync":
                    // ignore
                    break;
                case "$db":
                    Assert.equals(value, getDatabaseName());
                    break;
                default:
                    throw new MongoServerException("unknown subcommand: " + subCommand);
            }
        });

        List<Document> results = lastResults.get(channel);

        Document result;
        if (results != null && !results.isEmpty()) {
            result = results.get(results.size() - 1);
            if (result == null) {
                result = new Document();
            }
        } else {
            result = new Document();
            result.put("err", null);
            result.put("n", 0);
        }
        if (result.containsKey("writeErrors")) {
            @SuppressWarnings("unchecked")
            List<Document> writeErrors = (List<Document>) result.get("writeErrors");
            if (writeErrors.size() == 1) {
                result.putAll(CollectionUtils.getSingleElement(writeErrors));
                result.remove("writeErrors");
            }
        }
        Utils.markOkay(result);
        return result;
    }

    private Document commandResetError(Channel channel) {
        List<Document> results = lastResults.get(channel);
        if (results != null) {
            results.clear();
        }
        Document result = new Document();
        Utils.markOkay(result);
        return result;
    }

    private Document commandCount(String command, Document query) {
        MongoCollection<P> collection = resolveCollection(command, query);
        Document response = new Document();
        if (collection == null) {
            response.put("n", Integer.valueOf(0));
        } else {
            Document queryObject = (Document) query.get("query");
            int limit = getOptionalNumber(query, "limit", -1);
            int skip = getOptionalNumber(query, "skip", 0);
            response.put("n", Integer.valueOf(collection.count(queryObject, skip, limit)));
        }
        Utils.markOkay(response);
        return response;
    }

    private Document commandAggregate(String command, Document query, DatabaseResolver databaseResolver, Oplog oplog) {
        String collectionName = query.get(command).toString();
        MongoCollection<P> collection = resolveCollection(collectionName, false);
        Object pipelineObject = Aggregation.parse(query.get("pipeline"));
        List<Document> pipeline = Aggregation.parse(pipelineObject);
        if (!pipeline.isEmpty()) {
            Document changeStream = (Document) pipeline.get(0).get("$changeStream");
            if (changeStream != null) {
                Aggregation aggregation = getAggregation(pipeline.subList(1, pipeline.size()), query, databaseResolver, collection, oplog);
                aggregation.validate(query);
                return commandChangeStreamPipeline(query, oplog, collectionName, changeStream, aggregation);
            }
        }

        Aggregation aggregation = getAggregation(pipeline, query, databaseResolver, collection, oplog);
        return Utils.firstBatchCursorResponse(getFullCollectionNamespace(collectionName), aggregation.computeResult());
    }

    private Aggregation getAggregation(List<Document> pipeline, Document query, DatabaseResolver databaseResolver,
                                       MongoCollection<?> collection, Oplog oplog) {
        Aggregation aggregation = Aggregation.fromPipeline(pipeline, databaseResolver, this, collection, oplog);
        aggregation.validate(query);
        return aggregation;
    }

    private Document commandChangeStreamPipeline(Document query, Oplog oplog, String collectionName, Document changeStreamDocument,
                                                 Aggregation aggregation) {
        Document cursorDocument = (Document) query.get("cursor");
        int batchSize = (int) cursorDocument.getOrDefault("batchSize", 0);

        String namespace = getFullCollectionNamespace(collectionName);
        Cursor cursor = oplog.createCursor(changeStreamDocument, namespace, aggregation);
        return Utils.firstBatchCursorResponse(namespace, cursor.takeDocuments(batchSize), cursor);
    }

    private int getOptionalNumber(Document query, String fieldName, int defaultValue) {
        Number limitNumber = (Number) query.get(fieldName);
        return limitNumber != null ? limitNumber.intValue() : defaultValue;
    }

    @Override
    public QueryResult handleQuery(MongoQuery query) {
        clearLastStatus(query.getChannel());
        String collectionName = query.getCollectionName();
        MongoCollection<P> collection = resolveCollection(collectionName, false);
        if (collection == null) {
            return new QueryResult();
        }
        int numberToSkip = query.getNumberToSkip();
        int batchSize = query.getNumberToReturn();

        if (batchSize < -1) {
            // actually: request to close cursor automatically
            batchSize = -batchSize;
        }

        QueryParameters queryParameters = toQueryParameters(query, numberToSkip, batchSize);
        return collection.handleQuery(queryParameters);
    }

    @Override
    public void handleClose(Channel channel) {
        lastResults.remove(channel);
    }

    protected void clearLastStatus(Channel channel) {
        List<Document> results = lastResults.computeIfAbsent(channel, k -> new LimitedList<>(10));
        results.add(null);
    }

    private MongoCollection<P> resolveCollection(String command, Document query) {
        String collectionName = query.get(command).toString();
        return resolveCollection(collectionName, false);
    }

    @Override
    public MongoCollection<P> resolveCollection(String collectionName, boolean throwIfNotFound) {
        checkCollectionName(collectionName);
        MongoCollection<P> collection = collections.get(collectionName);
        if (collection == null && throwIfNotFound) {
            throw new MongoServerException("Collection [" + getFullCollectionNamespace(collectionName) + "] not found.");
        }
        return collection;
    }

    private void checkCollectionName(String collectionName) {
        if (collectionName.length() > Constants.MAX_NS_LENGTH) {
            throw new MongoServerError(10080, "ns name too long, max size is " + Constants.MAX_NS_LENGTH);
        }

        if (collectionName.isEmpty()) {
            throw new MongoServerError(16256, "Invalid ns [" + collectionName + "]");
        }
    }

    @Override
    public boolean isEmpty() {
        return collections.isEmpty();
    }

    private void addNamespace(MongoCollection<P> collection) {
        collections.put(collection.getCollectionName(), collection);
        if (!isSystemCollection(collection.getCollectionName())) {
            namespaces.addDocument(new Document("name", collection.getFullName()));
        }
    }

    protected void addIndex(Document indexDescription) {
        if (!indexDescription.containsKey("v")) {
            indexDescription.put("v", 2);
        }
        openOrCreateIndex(indexDescription);
    }

    protected MongoCollection<P> getOrCreateIndexesCollection() {
        if (indexes.get() == null) {
            MongoCollection<P> indexCollection = openOrCreateCollection(INDEXES_COLLECTION_NAME, CollectionOptions.withoutIdField());
            addNamespace(indexCollection);
            indexes.set(indexCollection);
        }
        return indexes.get();
    }

    private String extractCollectionNameFromNamespace(String namespace) {
        Assert.startsWith(namespace, databaseName);
        return namespace.substring(databaseName.length() + 1);
    }

    private void openOrCreateIndex(Document indexDescription) {
        String ns = indexDescription.get("ns").toString();
        String collectionName = extractCollectionNameFromNamespace(ns);
        MongoCollection<P> collection = resolveOrCreateCollection(collectionName);
        Index<P> index = openOrCreateIndex(collectionName, indexDescription);
        MongoCollection<P> indexesCollection = getOrCreateIndexesCollection();
        if (index != null) {
            collection.addIndex(index);
            indexesCollection.addDocumentIfMissing(indexDescription);
        }
    }

    private Index<P> openOrCreateIndex(String collectionName, Document indexDescription) {
        String indexName = (String) indexDescription.get("name");
        Document key = (Document) indexDescription.get("key");
        if (isPrimaryKeyIndex(key)) {
            if (!indexName.equals(PRIMARY_KEY_INDEX_NAME)) {
                log.warn("Ignoring primary key index with name '{}'", indexName);
                return null;
            }
            boolean ascending = isAscending(key.get(ID_FIELD));
            Index<P> index = openOrCreateIdIndex(collectionName, indexName, ascending);
            log.info("adding unique _id index for collection {}", collectionName);
            return index;
        } else {
            List<IndexKey> keys = new ArrayList<>();
            for (Entry<String, Object> entry : key.entrySet()) {
                String field = entry.getKey();
                boolean ascending = isAscending(entry.getValue());
                keys.add(new IndexKey(field, ascending));
            }

            boolean sparse = Utils.isTrue(indexDescription.get("sparse"));
            if (Utils.isTrue(indexDescription.get("unique"))) {
                log.info("adding {} unique index {} for collection {}", sparse ? "sparse" : "non-sparse", keys, collectionName);

                return openOrCreateUniqueIndex(collectionName, indexName, keys, sparse);
            } else {
                return openOrCreateSecondaryIndex(collectionName, indexName, keys, sparse);
            }
        }
    }

    @VisibleForExternalBackends
    protected boolean isPrimaryKeyIndex(Document key) {
        return key.keySet().equals(Set.of(ID_FIELD));
    }

    @VisibleForExternalBackends
    protected Index<P> openOrCreateSecondaryIndex(String collectionName, String indexName, List<IndexKey> keys, boolean sparse) {
        log.warn("adding secondary index with keys {} is not yet implemented. ignoring", keys);
        return new EmptyIndex<>(indexName, keys);
    }

    private static boolean isAscending(Object keyValue) {
        return Objects.equals(Utils.normalizeValue(keyValue), Double.valueOf(1.0));
    }

    private Index<P> openOrCreateIdIndex(String collectionName, String indexName, boolean ascending) {
        return openOrCreateUniqueIndex(collectionName, indexName, List.of(new IndexKey(ID_FIELD, ascending)), false);
    }

    protected abstract Index<P> openOrCreateUniqueIndex(String collectionName, String indexName, List<IndexKey> keys, boolean sparse);

    private List<Document> insertDocuments(Channel channel, String collectionName, List<Document> documents, Oplog oplog, boolean isOrdered) {
        clearLastStatus(channel);
        try {
            if (isSystemCollection(collectionName)) {
                throw new MongoServerError(16459, "attempt to insert in system namespace");
            }
            MongoCollection<P> collection = resolveOrCreateCollection(collectionName);
            List<Document> writeErrors = collection.insertDocuments(documents, isOrdered);
            oplog.handleInsert(collection.getFullName(), documents);
            if (!writeErrors.isEmpty()) {
                Document writeError = new Document(writeErrors.get(0));
                writeError.put("err", writeError.remove("errmsg"));
                putLastResult(channel, writeError);
            } else {
                Document result = new Document("n", 0);
                result.put("err", null);
                putLastResult(channel, result);
            }
            return writeErrors;
        } catch (MongoServerError e) {
            putLastError(channel, e);
            return List.of(toWriteError(0, e));
        }
    }

    private Document deleteDocuments(Channel channel, String collectionName, Document selector, int limit, Oplog oplog) {
        clearLastStatus(channel);
        try {
            if (isSystemCollection(collectionName)) {
                throw new InvalidNamespaceError(getFullCollectionNamespace(collectionName));
            }
            MongoCollection<P> collection = resolveCollection(collectionName, false);
            final int n;
            if (collection == null) {
                n = 0;
            } else {
                n = collection.deleteDocuments(selector, limit, oplog);
            }
            Document result = new Document("n", Integer.valueOf(n));
            putLastResult(channel, result);
            return result;
        } catch (MongoServerError e) {
            putLastError(channel, e);
            throw e;
        }
    }

    private Document updateDocuments(String collectionName, Document selector,
                                     Document update, ArrayFilters arrayFilters,
                                     boolean multi, boolean upsert, Oplog oplog) {

        if (isSystemCollection(collectionName)) {
            throw new MongoServerError(10156, "cannot update system collection");
        }

        MongoCollection<P> collection = resolveOrCreateCollection(collectionName);
        return collection.updateDocuments(selector, update, arrayFilters, multi, upsert, oplog);
    }

    private void putLastError(Channel channel, MongoServerException ex) {
        Document error = toError(channel, ex);
        putLastResult(channel, error);
    }

    private Document toWriteError(int index, MongoServerException e) {
        Document error = new Document();
        error.put("index", index);
        error.put("errmsg", e.getMessageWithoutErrorCode());
        if (e instanceof MongoServerError) {
            MongoServerError err = (MongoServerError) e;
            error.put("code", Integer.valueOf(err.getCode()));
            error.putIfNotNull("codeName", err.getCodeName());
        }
        return error;
    }

    private Document toError(Channel channel, MongoServerException ex) {
        Document error = new Document();
        error.put("err", ex.getMessageWithoutErrorCode());
        if (ex instanceof MongoServerError) {
            MongoServerError err = (MongoServerError) ex;
            error.put("code", Integer.valueOf(err.getCode()));
            error.putIfNotNull("codeName", err.getCodeName());
        }
        error.put("connectionId", channel.id().asShortText());
        return error;
    }

    protected void putLastResult(Channel channel, Document result) {
        List<Document> results = lastResults.get(channel);
        // list must not be empty
        Document last = results.get(results.size() - 1);
        Assert.isNull(last, () -> "last result already set: " + last);
        results.set(results.size() - 1, result);
    }

    private MongoCollection<P> createCollection(String collectionName, CollectionOptions options) {
        checkCollectionName(collectionName);
        if (collectionName.contains("$")) {
            throw new MongoServerError(10093, "cannot insert into reserved $ collection");
        }

        MongoCollection<P> collection = openOrCreateCollection(collectionName, options);
        addNamespace(collection);

        addIndex(getPrimaryKeyIndexDescription(collection.getFullName()));

        log.info("created collection {}", collection.getFullName());

        return collection;
    }

    protected abstract MongoCollection<P> openOrCreateCollection(String collectionName, CollectionOptions options);

    @Override
    public void drop(Oplog oplog) {
        log.debug("dropping {}", this);
        for (String collectionName : collections.keySet()) {
            if (!isSystemCollection(collectionName)) {
                dropCollection(collectionName, oplog);
            }
        }
        dropCollectionIfExists(INDEXES_COLLECTION_NAME, oplog);
        dropCollectionIfExists(NAMESPACES_COLLECTION_NAME, oplog);
    }

    private void dropCollectionIfExists(String collectionName, Oplog oplog) {
        if (collections.containsKey(collectionName)) {
            dropCollection(collectionName, oplog);
        }
    }

    @Override
    public void dropCollection(String collectionName, Oplog oplog) {
        MongoCollection<P> collection = resolveCollection(collectionName, true);
        dropAllIndexes(collection);
        collection.drop();
        unregisterCollection(collectionName);
        oplog.handleDropCollection(String.format("%s.%s", databaseName, collectionName));
    }

    private void dropAllIndexes(MongoCollection<P> collection) {
        MongoCollection<P> indexCollection = indexes.get();
        if (indexCollection == null) {
            return;
        }
        List<Document> indexesToDrop = new ArrayList<>();
        for (Document index : indexCollection.handleQuery(new Document("ns", collection.getFullName()))) {
            indexesToDrop.add(index);
        }
        for (Document indexToDrop : indexesToDrop) {
            dropIndex(collection, indexToDrop);
        }
    }

    @Override
    public void unregisterCollection(String collectionName) {
        MongoCollection<P> removedCollection = collections.remove(collectionName);
        namespaces.deleteDocuments(new Document("name", removedCollection.getFullName()), 1);
    }

    @Override
    public void moveCollection(MongoDatabase oldDatabase, MongoCollection<?> collection, String newCollectionName) {
        String oldFullName = collection.getFullName();
        oldDatabase.unregisterCollection(collection.getCollectionName());
        collection.renameTo(this, newCollectionName);
        // TODO resolve cast
        @SuppressWarnings("unchecked")
        MongoCollection<P> newCollection = (MongoCollection<P>) collection;
        MongoCollection<P> oldCollection = collections.put(newCollectionName, newCollection);
        Assert.isNull(oldCollection,
            () -> "Failed to register renamed collection. Another collection still existed: " + oldCollection);
        List<Document> newDocuments = new ArrayList<>();
        newDocuments.add(new Document("name", collection.getFullName()));

        indexes.get().updateDocuments(new Document("ns", oldFullName),
            new Document("$set", new Document("ns", newCollection.getFullName())),
            ArrayFilters.empty(), true, false, NoopOplog.get());

        namespaces.insertDocuments(newDocuments, true);
    }

    protected String getFullCollectionNamespace(String collectionName) {
        return getDatabaseName() + "." + collectionName;
    }

    static boolean isSystemCollection(String collectionName) {
        return collectionName.startsWith("system.");
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/ErrorCode.java
package de.bwaldvogel.mongo.exception;

public enum ErrorCode {
    BadValue(2),
    FailedToParse(9),
    TypeMismatch(14),
    IllegalOperation(20),
    IndexNotFound(27),
    PathNotViable(28),
    ConflictingUpdateOperators(40),
    CursorNotFound(43),
    NamespaceExists(48),
    DollarPrefixedFieldName(52),
    InvalidIdField(53),
    NotSingleValueField(54),
    CommandNotFound(59),
    ImmutableField(66),
    InvalidOptions(72),
    InvalidNamespace(73),
    IndexKeySpecsConflict(86),
    CannotIndexParallelArrays(171),
    ConversionFailure(241),
    DuplicateKey(11000),
    MergeStageNoMatchingDocument(13113),

    _15998(15998) {
        @Override
        public String getName() {
            return "Location" + getValue();
        }
    },
    _34471(34471) {
        @Override
        public String getName() {
            return "Location" + getValue();
        }
    },
    _40353(40353) {
        @Override
        public String getName() {
            return "Location" + getValue();
        }
    },
    _40390(40390) {
        @Override
        public String getName() {
            return "Location" + getValue();
        }
    },
    ;

    private final int id;

    ErrorCode(int id) {
        this.id = id;
    }

    public int getValue() {
        return id;
    }

    public String getName() {
        return name();
    }
}
<file_sep>/test-common/src/main/java/de/bwaldvogel/mongo/backend/AbstractPerformanceTest.java
package de.bwaldvogel.mongo.backend;

import static de.bwaldvogel.mongo.backend.TestUtils.json;

import org.bson.Document;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mongodb.client.model.UpdateOptions;

public abstract class AbstractPerformanceTest extends AbstractTest {

    private static final Logger log = LoggerFactory.getLogger(AbstractPerformanceTest.class);

    // https://github.com/bwaldvogel/mongo-java-server/issues/84
    @Test
    @EnabledIfSystemProperty(named = "run-mongo-java-server-performance-tests", matches = "true")
    public void testComplexUpsert() throws Exception {
        Document incUpdate = new Document();
        Document updateQuery = new Document("$inc", incUpdate);
        incUpdate.put("version", 1);
        for (int hour = 0; hour < 24; hour++) {
            for (int minute = 0; minute < 60; minute++) {
                incUpdate.put("data." + hour + "." + minute + ".requests", 0);
                incUpdate.put("data." + hour + "." + minute + ".responses", 0);
                incUpdate.put("data." + hour + "." + minute + ".duration", 0);
            }
        }

        for (int i = 0; i < 10; i++) {
            long start = System.currentTimeMillis();
            collection.updateOne(json("_id: 1"), updateQuery, new UpdateOptions().upsert(true));
            long stop = System.currentTimeMillis();
            log.info("Update took {} ms", stop - start);
        }
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/wire/OpCode.java
package de.bwaldvogel.mongo.wire;

import java.util.HashMap;
import java.util.Map;

public enum OpCode {
    OP_REPLY(1), // Reply to a client request. responseTo is set

    @Deprecated(/* no longer supported */)
    OP_UPDATE(2001), // update document

    @Deprecated(/* no longer supported */)
    OP_INSERT(2002), // insert new document

    RESERVED(2003), // formerly used for OP_GET_BY_OID

    OP_QUERY(2004), // query a collection

    @Deprecated(/* no longer supported */)
    OP_GET_MORE(2005), // Get more data from a query. See Cursors

    @Deprecated(/* no longer supported */)
    OP_DELETE(2006), // Delete documents

    @Deprecated(/* no longer supported */)
    OP_KILL_CURSORS(2007), // Tell database client is done with a cursor

    OP_MSG(2013); // Send a message using the format introduced in MongoDB 3.6

    private final int id;

    private static final Map<Integer, OpCode> byIdMap = new HashMap<>();

    static {
        for (final OpCode opCode : values()) {
            byIdMap.put(Integer.valueOf(opCode.id), opCode);
        }
    }

    OpCode(final int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public static OpCode getById(int id) {
        return byIdMap.get(Integer.valueOf(id));
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/stage/SortStage.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import java.util.stream.Stream;

import de.bwaldvogel.mongo.backend.DocumentComparator;
import de.bwaldvogel.mongo.bson.Document;

public class SortStage implements AggregationStage {

    private final DocumentComparator documentComparator;

    public SortStage(Document orderBy) {
        this.documentComparator = new DocumentComparator(orderBy);
    }

    @Override
    public String name() {
        return "$sort";
    }

    @Override
    public Stream<Document> apply(Stream<Document> stream) {
        return stream.sorted(documentComparator);
    }

}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/ProjectionTest.java
package de.bwaldvogel.mongo.backend;

import static de.bwaldvogel.mongo.TestUtils.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import org.junit.jupiter.api.Test;

import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.MongoServerError;

class ProjectionTest {

    @Test
    void testProjectDocument() throws Exception {
        assertThat(projectDocument(null, new Document(), null)).isNull();

        assertThat(projectDocument(json("_id: 100"), json("_id: 1"), "_id"))
            .isEqualTo(json("_id: 100"));

        assertThat(projectDocument(json("_id: 100, foo: 123"), json("foo: 1"), "_id"))
            .isEqualTo(json("_id: 100, foo: 123"));

        assertThat(projectDocument(json("_id: 100, foo: 123"), json("_id: 1"), "_id"))
            .isEqualTo(json("_id: 100"));

        assertThat(projectDocument(json("_id: 100, foo: 123"), json("_id: 0"), "_id"))
            .isEqualTo(json("foo: 123"));

        assertThat(projectDocument(json("_id: 100, foo: 123"), json("_id: 0, foo: 1"), "_id"))
            .isEqualTo(json("foo: 123"));

        assertThat(projectDocument(json("_id: 100, foo: 123, bar: 456"), json("_id: 0, foo: 1"), "_id"))
            .isEqualTo(json("foo: 123"));

        assertThat(projectDocument(json("_id: 100, foo: 123, bar: 456"), json("foo: 0"), "_id"))
            .isEqualTo(json("_id: 100, bar: 456"));

        assertThat(projectDocument(json("_id: 1, foo: {bar: 123, bla: 'x'}"), json("'foo.bar': 1"), "_id"))
            .isEqualTo(json("_id: 1, foo: {bar: 123}"));

        assertThat(projectDocument(json("_id: 1"), json("'foo.bar': 1"), "_id"))
            .isEqualTo(json("_id: 1"));

        assertThat(projectDocument(json("_id: 1, foo: {a: 'x', b: 'y', c: 'z'}"), json("'foo.a': 1, 'foo.c': 1"), "_id"))
            .isEqualTo(json("_id: 1, foo: {a: 'x', c: 'z'}"));

        assertThat(projectDocument(json("_id: 1, foo: {a: 'x', b: 'y', c: 'z'}"), json("'foo.b': 0"), "_id"))
            .isEqualTo(json("_id: 1, foo: {a: 'x', c: 'z'}"));
    }

    @Test
    void testInvalidMixedProjections() throws Exception {
        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> projectDocument(json("{}"), json("_id: 1, foo: 0"), "_id"))
            .withMessage("[Error 31253] Cannot do inclusion on field _id in exclusion projection");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> projectDocument(json("{}"), json("_id: 1, foo: 1, bar: 0"), "_id"))
            .withMessage("[Error 31253] Cannot do inclusion on field _id in exclusion projection");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> projectDocument(json("{}"), json("_id: 1, 'foo.a': 1, 'foo.b': 0"), "_id"))
            .withMessage("[Error 31253] Cannot do inclusion on field _id in exclusion projection");
    }

    @Test
    void testProjectIn() {
        Document document = json("_id: 1, students: [" +
            "{name: 'john', school: 'A', age: 10}, " +
            "{name: 'jess', school: 'B', age: 12}, " +
            "{name: 'jeff', school: 'A', age: 12}" +
            "]");

        assertThat(projectDocument(document, json("_id: 1, \"students.name\": 1")))
            .isEqualTo(json("_id:1, students: [{name: 'john'}, {name: 'jess'}, {name: 'jeff'}]"));

        Document document2 = json("_id: 1, students: {name: 'john', school: 'A', age: 10}");

        assertThat(projectDocument(document2, json("_id: 1, \"students.name\": 1")))
            .isEqualTo(json("_id:1, students: {name: 'john'}"));
    }

    @Test
    void testProjectOut() {
        Document document = json("_id: 1, students: [" +
            "{name: 'john', school: 'A', age: 10}, " +
            "{name: 'jess', school: 'B', age: 12}, " +
            "{name: 'jeff', school: 'A', age: 12}" +
            "]");

        assertThat(projectDocument(document, json("\"students.school\": 0,  \"students.age\": 0")))
            .isEqualTo(json("_id:1, students: [{name: 'john'}, {name: 'jess'}, {name: 'jeff'}]"));

        Document document2 = json("_id: 1, students: {name: 'john', school: 'A', age: 10}");

        assertThat(projectDocument(document2, json("\"students.school\": 0,  \"students.age\": 0")))
            .isEqualTo(json("_id:1, students: {name: 'john'}"));

    }


    @Test
    void testProjectMissingValue() throws Exception {
        assertThat(projectDocument(json("_id: 1"), json("'a.b': 1"), "_id"))
            .isEqualTo(json("_id: 1"));

        assertThat(projectDocument(json("_id: 1, a: null"), json("'a.b': 1"), "_id"))
            .isEqualTo(json("_id: 1"));

        assertThat(projectDocument(json("_id: 1, a: {b: null}"), json("'a.b': 1"), "_id"))
            .isEqualTo(json("_id: 1, a: {b: null}"));

        assertThat(projectDocument(json("_id: 1, a: {b: null}"), json("'a.c': 1"), "_id"))
            .isEqualTo(json("_id: 1, a: {}"));
    }

    @Test
    void testProjectListValues() throws Exception {
        assertThat(projectDocument(json("_id: 1, a: [1, 2, 3]"), json("'a.c': 1"), "_id"))
            .isEqualTo(json("_id: 1, a: []"));

        assertThat(projectDocument(json("_id: 1, a: [{x: 1}, 500, {y: 2}, {x: 3}]"), json("'a.x': 1"), "_id"))
            .isEqualTo(json("_id: 1, a: [{x: 1}, {}, {x: 3}]"));

        assertThat(projectDocument(json("_id: 1, a: [{x: 10, y: 100}, 100, {x: 20, y: 200}, {x: 3}, {z: 4}]"), json("'a.x': 1, 'a.y': 1"), "_id"))
            .isEqualTo(json("_id: 1, a: [{x: 10, y: 100}, {x: 20, y: 200}, {x: 3}, {}]"));

        assertThat(projectDocument(json("_id: 1, a: [{x: 10, y: 100}, 100, {x: 20, y: 200}, {x: 3}, {z: 4}]"), json("'a.z': 0"), "_id"))
            .isEqualTo(json("_id: 1, a: [{x: 10, y: 100}, 100, {x: 20, y: 200}, {x: 3}, {}]"));

        assertThat(projectDocument(json("_id: 1, a: [100, {z: 4}, {y: 3}, {x: 1, y: 4}]"), json("'a.x': 1, 'a.y': 1"), "_id"))
            .isEqualTo(json("_id: 1, a: [{}, {y: 3}, {x: 1, y: 4}]"));

        assertThat(projectDocument(json("_id: 1, a: [100, {z: 4}, {y: 3}, {x: 1, y: 4}]"), json("'a.z': 0"), "_id"))
            .isEqualTo(json("_id: 1, a: [100, {}, {y: 3}, {x: 1, y: 4}]"));

        assertThat(projectDocument(json("_id: 1, a: [100, {z: 4}, {y: 3}, {x: 1, y: 4}]"), json("'a.x': 0, 'a.z': 0"), "_id"))
            .isEqualTo(json("_id: 1, a: [100, {}, {y: 3}, {y: 4}]"));
    }

    @Test
    void testProjectListValuesWithPositionalOperator() throws Exception {
        Document document = json("_id: 1, students: [" +
            "{name: 'john', school: 'A', age: 10}, " +
            "{name: 'jess', school: 'B', age: 12}, " +
            "{name: 'jeff', school: 'A', age: 12}" +
            "]");

        assertThat(projectDocument(document, json("'students.$': 1"), "_id"))
            .isEqualTo(json("_id: 1, students: [{name: 'john', school: 'A', age: 10}]"));

        assertThat(projectDocument(document, json("'unknown.$': 1"), "_id"))
            .isEqualTo(json("_id: 1"));
    }

    @Test
    void testProjectWithElemMatch() throws Exception {
        Document document = json("_id: 1, students: [" +
            "{name: 'john', school: 'A', age: 10}, " +
            "{name: 'jess', school: 'B', age: 12}, " +
            "{name: 'jeff', school: 'A', age: 12}" +
            "]");

        assertThat(projectDocument(document, json("students: {$elemMatch: {school: 'B'}}"), "_id"))
            .isEqualTo(json("_id: 1, students: [{name: 'jess', school: 'B', age: 12}]"));

        assertThat(projectDocument(document, json("students: {$elemMatch: {school: 'A', age: {$gt: 10}}}"), "_id"))
            .isEqualTo(json("_id: 1, students: [{name: 'jeff', school: 'A', age: 12}]"));

        assertThat(projectDocument(document, json("students: {$elemMatch: {school: 'C'}}"), "_id"))
            .isEqualTo(json("_id: 1"));

        assertThat(projectDocument(json("_id: 1, students: [1, 2, 3]"), json("students: {$elemMatch: {school: 'C'}}"), "_id"))
            .isEqualTo(json("_id: 1"));

        assertThat(projectDocument(json("_id: 1, students: {school: 'C'}"), json("students: {$elemMatch: {school: 'C'}}"), "_id"))
            .isEqualTo(json("_id: 1"));

        assertThat(projectDocument(document, json("students: {$elemMatch: {school: 'A'}}"), "_id"))
            .isEqualTo(json("_id: 1, students: [{name: 'john', school: 'A', age: 10}]"));
    }


    @Test
    void testProjectWithSlice() throws Exception {
        Document document = json("_id: 1, values: [1, 2, 3, 4], value: 'other'");

        assertThat(projectDocument(document, json("values: {$slice: 2}"), "_id"))
            .isEqualTo(json("_id: 1, values: [1, 2]"));

        assertThat(projectDocument(document, json("values: {$slice: 20}"), "_id"))
            .isEqualTo(json("_id: 1, values: [1, 2, 3, 4]"));

        assertThat(projectDocument(document, json("values: {$slice: -2}"), "_id"))
            .isEqualTo(json("_id: 1, values: [3, 4]"));

        assertThat(projectDocument(document, json("values: {$slice: -10}"), "_id"))
            .isEqualTo(json("_id: 1, values: [1, 2, 3, 4]"));

        assertThat(projectDocument(document, json("values: {$slice: [-2, 1]}"), "_id"))
            .isEqualTo(json("_id: 1, values: [3]"));

        assertThat(projectDocument(document, json("value: {$slice: 2}"), "_id"))
            .isEqualTo(json("_id: 1, value: 'other'"));
    }

    private Document projectDocument(Document document, Document fields) {
        return projectDocument(document, fields, "_id");
    }

    private Document projectDocument(Document document, Document fields, String idField) {
        return new Projection(fields, idField).projectDocument(document);
    }

}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/aggregation/stage/AbstractLookupStageTest.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import de.bwaldvogel.mongo.MongoDatabase;

@ExtendWith(MockitoExtension.class)
public abstract class AbstractLookupStageTest {

    @Mock
    MongoDatabase database;

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/CannotIndexParallelArraysError.java
package de.bwaldvogel.mongo.exception;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public class CannotIndexParallelArraysError extends KeyConstraintError {

    private static final long serialVersionUID = 1L;

    public CannotIndexParallelArraysError(Collection<String> paths) {
        super(ErrorCode.CannotIndexParallelArrays, "cannot index parallel arrays " + formatPaths(paths));
    }

    private static String formatPaths(Collection<String> paths) {
        List<String> firstTwoPaths = paths.stream()
            .map(v -> "[" + v + "]")
            .limit(2)
            .collect(Collectors.toList());

        Collections.reverse(firstTwoPaths);
        return String.join(" ", firstTwoPaths);
    }
}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/InMemoryCursorTest.java
package de.bwaldvogel.mongo.backend;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.junit.jupiter.api.Test;

import de.bwaldvogel.mongo.bson.Document;

class InMemoryCursorTest {

    @Test
    void testIsEmpty() {
        InMemoryCursor cursor = new InMemoryCursor(1L, List.of(new Document()));
        assertThat(cursor.isEmpty()).isFalse();
    }

    @Test
    void testGetCursorId() {
        Cursor cursor = new InMemoryCursor(1L, List.of(new Document()));
        assertThat(cursor.getId()).isEqualTo(1);
    }

    @Test
    void testTakeDocuments() {
        List<Document> documents = IntStream.range(0, 22)
            .mapToObj(idx -> new Document("number", idx + 1))
            .collect(Collectors.toList());

        InMemoryCursor cursor = new InMemoryCursor(1L, documents);
        assertThat(cursor.takeDocuments(1)).containsExactly(documents.get(0));
        assertThat(cursor.takeDocuments(2)).containsExactly(documents.get(1), documents.get(2));
        assertThat(cursor.takeDocuments(1)).containsExactly(documents.get(3));
        assertThat(cursor.takeDocuments(6)).containsExactlyElementsOf(documents.subList(4, 10));
        assertThat(cursor.takeDocuments(10)).containsExactlyElementsOf(documents.subList(10, 20));
        assertThat(cursor.takeDocuments(10)).containsExactly(documents.get(20), documents.get(21));
        assertThat(cursor.takeDocuments(10)).isEmpty();

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> cursor.takeDocuments(0))
            .withMessage("Illegal number to return: 0");
    }

    @Test
    void testTakeDocuments_returnsUnmodifiableList() {
        InMemoryCursor cursor = new InMemoryCursor(123L, List.of(new Document(), new Document()));
        List<Document> documents = cursor.takeDocuments(5);
        assertThatExceptionOfType(UnsupportedOperationException.class)
            .isThrownBy(() -> documents.remove(0));
    }

    @Test
    void testToString() throws Exception {
        assertThat(new InMemoryCursor(123L, List.of(new Document())))
            .hasToString("InMemoryCursor(id: 123)");
    }

}
<file_sep>/test-common/src/main/java/de/bwaldvogel/mongo/repository/AccountRepository.java
package de.bwaldvogel.mongo.repository;

import org.bson.types.ObjectId;
import org.springframework.data.repository.PagingAndSortingRepository;

import de.bwaldvogel.mongo.entity.Account;

public interface AccountRepository extends PagingAndSortingRepository<Account, ObjectId> {
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/stage/AbstractLookupStage.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import java.util.Set;

import de.bwaldvogel.mongo.backend.Utils;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.bson.Json;
import de.bwaldvogel.mongo.exception.FailedToParseException;

abstract class AbstractLookupStage implements AggregationStage {

    static final String FROM = "from";
    static final String AS = "as";

    @Override
    public String name() {
        return "$lookup";
    }

    String readStringConfigurationProperty(Document configuration, String name) {
        Object value = configuration.get(name);
        if (value == null) {
            throw new FailedToParseException("missing '" + name + "' option to " + name() + " stage specification: " + configuration);
        }
        if (value instanceof String) {
            return (String) value;
        }
        throw new FailedToParseException("'" + name + "' option to " + name() + " must be a string, but was type " + Utils.describeType(value));
    }

    Document readOptionalDocumentArgument(Document configuration, String name) {
        Object value = configuration.get(name);
        if (value == null) {
            return new Document();
        }
        if (value instanceof Document) {
            return (Document) value;
        }
        throw new FailedToParseException(name() + " argument '" + name + ": " + Json.toJsonValue(value) + "' must be an object, is type " + Utils.describeType(value));
    }

    void ensureAllConfigurationPropertiesAreKnown(Document configuration, Set<String> configurationKeys) {
        for (String name : configuration.keySet()) {
            if (!configurationKeys.contains(name)) {
                String message = "unknown argument to " + name() + ": " + name;
                throw new FailedToParseException(message);
            }
        }
    }

}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/bson/BsonJavaScriptTest.java
package de.bwaldvogel.mongo.bson;

import org.junit.jupiter.api.Test;

import nl.jqno.equalsverifier.EqualsVerifier;

class BsonJavaScriptTest {

    @Test
    void testEqualsAndHashCode() throws Exception {
        EqualsVerifier.forClass(BsonJavaScript.class).verify();
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/wire/QueryFlag.java
package de.bwaldvogel.mongo.wire;

public enum QueryFlag {
    TAILABLE(1),
    SLAVE_OK(2),
    OPLOG_REPLAY(3),
    NO_CURSOR_TIMEOUT(4),
    AWAIT_DATA(5),
    EXHAUST(6),
    PARTIAL(7);

    private final int value;

    QueryFlag(int bit) {
        this.value = 1 << bit;
    }

    public boolean isSet(int flags) {
        return (flags & value) == value;
    }

    public int removeFrom(int flags) {
        return flags - value;
    }

}
<file_sep>/test-common/src/main/java/de/bwaldvogel/mongo/backend/TestSubscriber.java
package de.bwaldvogel.mongo.backend;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class TestSubscriber<T> implements Subscriber<T> {

    private static final Logger log = LoggerFactory.getLogger(TestSubscriber.class);

    private final CountDownLatch countDownLatch = new CountDownLatch(1);
    private T value;
    private Throwable throwable;
    private Subscription subscription;

    @Override
    public void onSubscribe(Subscription subscription) {
        this.subscription = subscription;
        subscription.request(1);
    }

    @Override
    public void onNext(T value) {
        log.debug("onNext: {}", value);
        Assert.isNull(this.value, () -> "Got a second value: " + this.value + " and " + value);
        this.value = value;
        subscription.cancel();
        countDownLatch.countDown();
    }

    @Override
    public void onError(Throwable throwable) {
        log.error("onError", throwable);
        this.throwable = throwable;
    }

    @Override
    public void onComplete() {
        log.info("onComplete", throwable);
    }

    T awaitSingleValue() throws Exception {
        boolean success = countDownLatch.await(30, TimeUnit.SECONDS);
        Assert.isTrue(success, () -> "Failed waiting countdown latch");
        if (throwable != null) {
            throw new RuntimeException(throwable);
        }
        Assert.notNull(value, () -> "Got no value yet");
        return value;
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/AbstractMongoCollection.java
package de.bwaldvogel.mongo.backend;

import static de.bwaldvogel.mongo.backend.Constants.ID_FIELD;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.bwaldvogel.mongo.MongoCollection;
import de.bwaldvogel.mongo.MongoDatabase;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.bson.ObjectId;
import de.bwaldvogel.mongo.exception.ConflictingUpdateOperatorsException;
import de.bwaldvogel.mongo.exception.ErrorCode;
import de.bwaldvogel.mongo.exception.FailedToParseException;
import de.bwaldvogel.mongo.exception.ImmutableFieldException;
import de.bwaldvogel.mongo.exception.IndexBuildFailedException;
import de.bwaldvogel.mongo.exception.IndexKeySpecsConflictException;
import de.bwaldvogel.mongo.exception.IndexNotFoundException;
import de.bwaldvogel.mongo.exception.InvalidIdFieldError;
import de.bwaldvogel.mongo.exception.MongoServerError;
import de.bwaldvogel.mongo.exception.MongoServerException;
import de.bwaldvogel.mongo.exception.PathNotViableException;
import de.bwaldvogel.mongo.oplog.Oplog;

public abstract class AbstractMongoCollection<P> implements MongoCollection<P> {

    private static final Logger log = LoggerFactory.getLogger(AbstractMongoCollection.class);

    private final UUID uuid = UUID.randomUUID();

    private MongoDatabase database;
    private String collectionName;
    private final List<Index<P>> indexes = new ArrayList<>();
    private final QueryMatcher matcher = new DefaultQueryMatcher();
    protected final CollectionOptions options;
    protected final CursorRegistry cursorRegistry;

    protected AbstractMongoCollection(MongoDatabase database, String collectionName, CollectionOptions options,
                                      CursorRegistry cursorRegistry) {
        this.database = Objects.requireNonNull(database);
        this.collectionName = Objects.requireNonNull(collectionName);
        this.options = Objects.requireNonNull(options);
        this.cursorRegistry = cursorRegistry;
    }

    @Override
    public UUID getUuid() {
        return uuid;
    }

    protected boolean documentMatchesQuery(Document document, Document query) {
        return matcher.matches(document, query);
    }

    protected QueryResult queryDocuments(Document query, Document orderBy, int numberToSkip, int limit, int batchSize,
                                         Document fieldSelector) {
        for (Index<P> index : indexes) {
            if (index.canHandle(query)) {
                Iterable<P> positions = index.getPositions(query);
                return matchDocuments(query, positions, orderBy, numberToSkip, limit, batchSize, fieldSelector);
            }
        }

        return matchDocuments(query, orderBy, numberToSkip, limit, batchSize, fieldSelector);
    }

    protected abstract QueryResult matchDocuments(Document query, Document orderBy, int numberToSkip,
                                                  int numberToReturn, int batchSize, Document fieldSelector);

    protected QueryResult matchDocumentsFromStream(Stream<Document> documentStream, Document query, Document orderBy,
                                                   int numberToSkip, int limit, int batchSize, Document fieldSelector) {
        Comparator<Document> documentComparator = deriveComparator(orderBy);
        return matchDocumentsFromStream(query, documentStream, numberToSkip, limit, batchSize, documentComparator, fieldSelector);
    }

    protected QueryResult matchDocumentsFromStream(Document query, Stream<Document> documentStream,
                                                   int numberToSkip, int limit, int batchSize,
                                                   Comparator<Document> documentComparator,
                                                   Document fieldSelector) {
        documentStream = documentStream
            .filter(document -> documentMatchesQuery(document, query));

        if (documentComparator != null) {
            documentStream = documentStream.sorted(documentComparator);
        }

        if (numberToSkip > 0) {
            documentStream = documentStream.skip(numberToSkip);
        }

        if (limit > 0) {
            documentStream = documentStream.limit(limit);
        }

        if (fieldSelector != null && !fieldSelector.keySet().isEmpty()) {
            Projection projection = new Projection(fieldSelector, getIdField());
            documentStream = documentStream.map(projection::projectDocument);
        }

        List<Document> matchedDocuments = documentStream.collect(Collectors.toList());
        return createQueryResult(matchedDocuments, batchSize);
    }

    protected QueryResult matchDocuments(Document query, Iterable<P> positions, Document orderBy,
                                         int numberToSkip, int limit, int batchSize,
                                         Document fieldSelector) {
        Stream<Document> documentStream = StreamSupport.stream(positions.spliterator(), false)
            .map(this::getDocument);

        return matchDocumentsFromStream(documentStream, query, orderBy, numberToSkip, limit, batchSize, fieldSelector);
    }

    protected static boolean isNaturalDescending(Document orderBy) {
        if (orderBy != null && !orderBy.keySet().isEmpty()) {
            if (orderBy.keySet().iterator().next().equals("$natural")) {
                Number sortValue = (Number) orderBy.get("$natural");
                if (sortValue.intValue() == -1) {
                    return true;
                }

                if (sortValue.intValue() != 1) {
                    throw new IllegalArgumentException("Illegal sort value: " + sortValue);
                }
            }
        }
        return false;
    }

    protected static DocumentComparator deriveComparator(Document orderBy) {
        if (orderBy != null && !orderBy.keySet().isEmpty()) {
            if (orderBy.keySet().iterator().next().equals("$natural")) {
                // already sorted
            } else {
                return new DocumentComparator(orderBy);
            }
        }
        return null;
    }

    protected abstract Document getDocument(P position);

    protected abstract void updateDataSize(int sizeDelta);

    protected abstract int getDataSize();

    protected abstract P addDocumentInternal(Document document);

    @Override
    public void addDocument(Document document) {
        if (document.get(ID_FIELD) instanceof Collection) {
            throw new InvalidIdFieldError("The '_id' value cannot be of type array");
        }

        if (!document.containsKey(ID_FIELD) && !isSystemCollection()) {
            ObjectId generatedObjectId = new ObjectId();
            log.debug("Generated {} for {} in {}", generatedObjectId, document, this);
            document.put(ID_FIELD, generatedObjectId);
        }

        for (Index<P> index : indexes) {
            index.checkAdd(document, this);
        }

        P position = addDocumentInternal(document);

        for (Index<P> index : indexes) {
            index.add(document, position, this);
        }

        if (tracksDataSize()) {
            updateDataSize(Utils.calculateSize(document));
        }
    }

    @Override
    public MongoDatabase getDatabase() {
        return database;
    }

    @Override
    public String getCollectionName() {
        return collectionName;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "(" + getFullName() + ")";
    }

    @Override
    public void addIndex(Index<P> index) {
        Index<P> existingIndex = findByName(index.getName());
        if (existingIndex != null) {
            if (!existingIndex.hasSameOptions(index)) {
                throw new IndexKeySpecsConflictException(index, existingIndex);
            }
            log.debug("Index with name '{}' already exists", index.getName());
            return;
        }
        if (index.isEmpty()) {
            try {
                streamAllDocumentsWithPosition().forEach(documentWithPosition -> {
                    Document document = documentWithPosition.getDocument();
                    index.checkAdd(document, this);
                });
                streamAllDocumentsWithPosition().forEach(documentWithPosition -> {
                    Document document = documentWithPosition.getDocument();
                    P position = documentWithPosition.getPosition();
                    index.add(document, position, this);
                });
            } catch (MongoServerError e) {
                throw new IndexBuildFailedException(e, this);
            }
        } else {
            log.debug("Index is not empty");
        }
        indexes.add(index);
    }

    private Index<P> findByName(String indexName) {
        return indexes.stream()
            .filter(index -> index.getName().equals(indexName))
            .findFirst()
            .orElse(null);
    }

    @Override
    public void drop() {
        log.debug("Dropping collection {}", getFullName());
        Assert.isEmpty(indexes);
    }

    @Override
    public void dropIndex(String indexName) {
        log.debug("Dropping index '{}'", indexName);
        List<Index<P>> indexesToDrop = indexes.stream()
            .filter(index -> index.getName().equals(indexName))
            .collect(Collectors.toList());
        if (indexesToDrop.isEmpty()) {
            throw new IndexNotFoundException("index not found with name [" + indexName + "]");
        }
        Index<P> indexToDrop = CollectionUtils.getSingleElement(indexesToDrop);
        indexToDrop.drop();
        indexes.remove(indexToDrop);
    }

    private void modifyField(Document document, String modifier, Document update, ArrayFilters arrayFilters,
                             Integer matchPos, boolean isUpsert) {
        Document change = (Document) update.get(modifier);
        UpdateOperator updateOperator = getUpdateOperator(modifier, change);
        FieldUpdates updates = new FieldUpdates(document, updateOperator, getIdField(), isUpsert, matchPos, arrayFilters);
        updates.apply(change, modifier);
    }

    protected String getIdField() {
        return options.getIdField();
    }

    private UpdateOperator getUpdateOperator(String modifier, Document change) {
        final UpdateOperator op;
        try {
            op = UpdateOperator.fromValue(modifier);
        } catch (IllegalArgumentException e) {
            throw new FailedToParseException("Unknown modifier: " + modifier + ". Expected a valid update modifier or pipeline-style update specified as an array");
        }

        return op;
    }

    private void applyUpdate(Document oldDocument, Document newDocument) {
        if (newDocument.equals(oldDocument)) {
            return;
        }

        Object oldId = oldDocument.get(getIdField());
        Object newId = newDocument.get(getIdField());

        if (newId != null && oldId != null && !Utils.nullAwareEquals(oldId, newId)) {
            throw new ImmutableFieldException("After applying the update, the (immutable) field '_id' was found to have been altered to _id: " + newId);
        }

        if (newId == null && oldId != null) {
            newDocument.put(getIdField(), oldId);
        }

        newDocument.cloneInto(oldDocument);
    }

    Object deriveDocumentId(Document selector) {
        Object value = selector.get(getIdField());
        if (value != null) {
            if (!Utils.containsQueryExpression(value)) {
                return value;
            } else {
                return deriveIdFromExpression(value);
            }
        }
        return new ObjectId();
    }

    private Object deriveIdFromExpression(Object value) {
        Document expression = (Document) value;
        for (String key : expression.keySet()) {
            Object expressionValue = expression.get(key);
            if (key.equals("$in")) {
                Collection<?> list = (Collection<?>) expressionValue;
                if (!list.isEmpty()) {
                    return list.iterator().next();
                }
            }
        }
        // fallback to random object id
        return new ObjectId();
    }

    private Document calculateUpdateDocument(Document oldDocument, Document update, ArrayFilters arrayFilters,
                                             Integer matchPos, boolean isUpsert) {

        int numStartsWithDollar = 0;
        for (String key : update.keySet()) {
            if (key.startsWith("$")) {
                numStartsWithDollar++;
            }
        }

        Document newDocument = new Document();
        if (getIdField() != null) {
            newDocument.put(getIdField(), oldDocument.get(getIdField()));
        }

        if (numStartsWithDollar == update.keySet().size()) {
            validateUpdateQuery(update);
            oldDocument.cloneInto(newDocument);
            for (String key : update.keySet()) {
                modifyField(newDocument, key, update, arrayFilters, matchPos, isUpsert);
            }
        } else if (numStartsWithDollar == 0) {
            applyUpdate(newDocument, update);
        } else {
            throw new MongoServerException("illegal update: " + update);
        }

        Utils.validateFieldNames(newDocument);
        return newDocument;
    }

    static void validateUpdateQuery(Document update) {
        Set<String> allModifiedPaths = new LinkedHashSet<>();
        for (Object value : update.values()) {
            Document modification = (Document) value;
            for (String path : modification.keySet()) {
                for (String otherPath : allModifiedPaths) {
                    String commonPathPrefix = Utils.getShorterPathIfPrefix(path, otherPath);
                    if (commonPathPrefix != null) {
                        throw new ConflictingUpdateOperatorsException(path, commonPathPrefix);
                    }
                }
                allModifiedPaths.add(path);
            }
        }
    }

    @Override
    public Document findAndModify(Document query) {
        boolean returnNew = Utils.isTrue(query.get("new"));

        if (!query.containsKey("remove") && !query.containsKey("update")) {
            throw new FailedToParseException("Either an update or remove=true must be specified");
        }

        Document queryObject = new Document();

        if (query.containsKey("query")) {
            queryObject.put("query", query.get("query"));
        } else {
            queryObject.put("query", new Document());
        }

        if (query.containsKey("sort")) {
            queryObject.put("orderby", query.get("sort"));
        }

        Document lastErrorObject = null;
        Document returnDocument = null;
        boolean matchingDocument = false;
        for (Document document : handleQuery(queryObject, 0, 1)) {
            matchingDocument = true;
            if (Utils.isTrue(query.get("remove"))) {
                removeDocument(document);
                returnDocument = document;
            } else if (query.get("update") != null) {
                Document updateQuery = (Document) query.get("update");

                Integer matchPos = matcher.matchPosition(document, (Document) queryObject.get("query"));

                ArrayFilters arrayFilters = ArrayFilters.parse(query, updateQuery);

                final Document oldDocument;
                try {
                    oldDocument = updateDocument(document, updateQuery, arrayFilters, matchPos);
                } catch (MongoServerError e) {
                    if (e.shouldPrefixCommandContext()) {
                        throw new FindAndModifyPlanExecutorError(e);

                    } else {
                        throw e;
                    }
                }

                if (returnNew) {
                    returnDocument = document;
                } else {
                    returnDocument = oldDocument;
                }
                lastErrorObject = new Document("updatedExisting", Boolean.TRUE);
                lastErrorObject.put("n", Integer.valueOf(1));
            }
        }

        if (!matchingDocument && Utils.isTrue(query.get("upsert"))) {
            Document selector = (Document) query.get("query");
            Document updateQuery = (Document) query.get("update");
            ArrayFilters arrayFilters = ArrayFilters.parse(query, updateQuery);
            Document newDocument = handleUpsert(updateQuery, selector, arrayFilters);
            if (returnNew) {
                returnDocument = newDocument;
            } else {
                returnDocument = null;
            }
        }

        Document fields = (Document) query.get("fields");
        if (fields != null) {
            returnDocument = new Projection(fields, getIdField()).projectDocument(returnDocument);
        }

        Document result = new Document();
        if (lastErrorObject != null) {
            result.put("lastErrorObject", lastErrorObject);
        }
        result.put("value", returnDocument);
        Utils.markOkay(result);
        return result;
    }

    public static class FindAndModifyPlanExecutorError extends MongoServerError {

        private static final long serialVersionUID = 1L;

        private static final String PREFIX = "Plan executor error during findAndModify :: caused by :: ";

        private FindAndModifyPlanExecutorError(MongoServerError cause) {
            super(cause.getCode(), cause.getCodeName(), PREFIX + cause.getMessageWithoutErrorCode(), cause);
        }

        @Override
        public synchronized MongoServerError getCause() {
            return (MongoServerError) super.getCause();
        }
    }

    @Override
    public QueryResult handleQuery(QueryParameters queryParameters) {
        final Document query;
        final Document orderBy;
        Document querySelector = queryParameters.getQuerySelector();
        if (querySelector.containsKey("query")) {
            query = (Document) querySelector.get("query");
            orderBy = (Document) querySelector.get("orderby");
        } else if (querySelector.containsKey("$query")) {
            query = (Document) querySelector.get("$query");
            orderBy = (Document) querySelector.get("$orderby");
        } else {
            query = querySelector;
            orderBy = null;
        }

        return queryDocuments(query, orderBy, queryParameters.getNumberToSkip(), queryParameters.getLimit(),
            queryParameters.getBatchSize(), queryParameters.getProjection());
    }

    @Override
    public List<Document> insertDocuments(List<Document> documents, boolean isOrdered) {
        int index = 0;
        List<Document> writeErrors = new ArrayList<>();
        for (Document document : documents) {
            try {
                addDocument(document);
            } catch (MongoServerError e) {
                writeErrors.add(toErrorDocument(e, index));
                if (isOrdered) {
                    break;
                }
            }
            index++;
        }
        return writeErrors;
    }

    private static Document toErrorDocument(MongoServerError e, int index) {
        Document error = new Document();
        error.put("index", index);
        error.put("errmsg", e.getMessageWithoutErrorCode());
        error.put("code", Integer.valueOf(e.getCode()));
        error.putIfNotNull("codeName", e.getCodeName());
        return error;
    }

    @Override
    public Document handleDistinct(Document query) {
        String key = (String) query.get("key");
        Document filter = (Document) query.getOrDefault("query", new Document());
        Set<Object> values = new TreeSet<>(ValueComparator.ascWithoutListHandling());

        for (Document document : queryDocuments(filter, null, 0, 0, 0, null)) {
            Object value = Utils.getSubdocumentValueCollectionAware(document, key);
            if (!(value instanceof Missing)) {
                if (value instanceof Collection) {
                    values.addAll((Collection<?>) value);
                } else {
                    values.add(value);
                }
            }
        }

        Document response = new Document("values", values);
        Utils.markOkay(response);
        return response;
    }

    @Override
    public int deleteDocuments(Document selector, int limit, Oplog oplog) {
        List<Object> deletedDocumentIds = new ArrayList<>();
        for (Document document : handleQuery(selector, 0, limit)) {
            if (limit > 0 && deletedDocumentIds.size() >= limit) {
                throw new MongoServerException("internal error: too many elements (" + deletedDocumentIds.size() + " >= " + limit + ")");
            }
            deletedDocumentIds.add(document.get(getIdField()));
            removeDocument(document);
        }
        oplog.handleDelete(getFullName(), selector, deletedDocumentIds);
        return deletedDocumentIds.size();
    }

    @Override
    public Document updateDocuments(Document selector, Document updateQuery, ArrayFilters arrayFilters,
                                    boolean isMulti, boolean isUpsert, Oplog oplog) {
        if (isMulti) {
            for (String key : updateQuery.keySet()) {
                if (!key.startsWith("$")) {
                    throw new MongoServerError(10158, "multi update only works with $ operators");
                }
            }
        }

        int nMatched = 0;
        List<Object> updatedIds = new ArrayList<>();
        for (Document document : queryDocuments(selector, null, 0, 0, 0, null)) {
            Integer matchPos = matcher.matchPosition(document, selector);
            Document oldDocument = updateDocument(document, updateQuery, arrayFilters, matchPos);
            if (!Utils.nullAwareEquals(oldDocument, document)) {
                updatedIds.add(document.get(getIdField()));
            }
            nMatched++;

            if (!isMulti) {
                break;
            }
        }

        Document result = new Document();

        // insert?
        if (nMatched == 0 && isUpsert) {
            Document newDocument = handleUpsert(updateQuery, selector, arrayFilters);
            result.put("upserted", newDocument.get(getIdField()));
            oplog.handleInsert(getFullName(), List.of(newDocument));
        } else {
            oplog.handleUpdate(getFullName(), selector, updateQuery, updatedIds);
        }

        result.put("n", Integer.valueOf(nMatched));
        result.put("nModified", Integer.valueOf(updatedIds.size()));
        return result;
    }

    private Document updateDocument(Document document, Document updateQuery,
                                    ArrayFilters arrayFilters, Integer matchPos) {
        Document oldDocument = document.cloneDeeply();

        Document newDocument = calculateUpdateDocument(document, updateQuery, arrayFilters, matchPos, false);

        if (!newDocument.equals(oldDocument)) {
            for (Index<P> index : indexes) {
                index.checkUpdate(oldDocument, newDocument, this);
            }
            P position = getSinglePosition(oldDocument);
            for (Index<P> index : indexes) {
                index.updateInPlace(oldDocument, newDocument, position, this);
            }

            if (tracksDataSize()) {
                int oldSize = Utils.calculateSize(oldDocument);
                int newSize = Utils.calculateSize(newDocument);
                updateDataSize(newSize - oldSize);
            }

            // only keep fields that are also in the updated document
            Set<String> fields = new LinkedHashSet<>(document.keySet());
            fields.removeAll(newDocument.keySet());
            for (String key : fields) {
                document.remove(key);
            }

            // update the fields
            for (String key : newDocument.keySet()) {
                if (key.contains(".")) {
                    throw new MongoServerException(
                        "illegal field name. must not happen as it must be caught by the driver");
                }
                document.put(key, newDocument.get(key));
            }
            handleUpdate(position, oldDocument, document);
        }
        return oldDocument;
    }

    private P getSinglePosition(Document document) {
        if (indexes.isEmpty()) {
            return findDocumentPosition(document);
        }
        Set<P> positions = indexes.stream()
            .map(index -> index.getPosition(document))
            .filter(Objects::nonNull)
            .collect(Collectors.toSet());
        return CollectionUtils.getSingleElement(positions);
    }

    protected abstract void handleUpdate(P position, Document oldDocument, Document newDocument);

    private Document handleUpsert(Document updateQuery, Document selector, ArrayFilters arrayFilters) {
        Document document = convertSelectorToDocument(selector);
        Document newDocument = calculateUpdateDocument(document, updateQuery, arrayFilters, null, true);
        newDocument.computeIfAbsent(getIdField(), k -> deriveDocumentId(selector));
        addDocument(newDocument);
        return newDocument;
    }

    /**
     * convert selector used in an upsert statement into a document
     */
    Document convertSelectorToDocument(Document selector) {
        Document document = new Document();
        convertSelectorToDocument(selector, document);
        return document;
    }

    private void convertSelectorToDocument(Document selector, Document document) {
        for (Map.Entry<String, Object> entry : selector.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();

            if (key.equals("$and")) {
                List<Document> andValues = (List<Document>) value;
                for (Document andValue : andValues) {
                    convertSelectorToDocument(andValue, document);
                }
                continue;
            } else if (key.equals("$or")) {
                List<Document> orValues = (List<Document>) value;
                if (orValues.size() == 1) {
                    convertSelectorToDocument(orValues.get(0), document);
                }
                continue;
            } else if (key.startsWith("$")) {
                continue;
            } else if (value instanceof Document) {
                Document documentValue = (Document) value;
                if (documentValue.keySet().equals(Set.of("$eq"))) {
                    changeSubdocumentValueOrThrow(document, key, documentValue.get("$eq"));
                }
            }

            if (!Utils.containsQueryExpression(value)) {
                changeSubdocumentValueOrThrow(document, key, value);
            }
        }
    }

    private static void changeSubdocumentValueOrThrow(Document document, String key, Object value) {
        try {
            Object previousValue = Utils.changeSubdocumentValue(document, key, value, (AtomicReference<Integer>) null);
            if (previousValue != null) {
                throw new MongoServerError(ErrorCode.NotSingleValueField, "cannot infer query fields to set, path '" + key + "' is matched twice");
            }
        } catch (PathNotViableException e) {
            String violatingKey = e.getField();
            List<String> keyPathFragments = Utils.splitPath(key);
            int indexOfViolatingKey = keyPathFragments.indexOf(violatingKey);
            List<String> violatingKeyPathFragments = keyPathFragments.subList(0, indexOfViolatingKey);
            String violatingKeyPath = Utils.joinPath(violatingKeyPathFragments);
            throw new MongoServerError(ErrorCode.NotSingleValueField, "cannot infer query fields to set, both paths '" + key + "' and '" + violatingKeyPath + "' are matched");
        }
    }

    @Override
    public List<Index<P>> getIndexes() {
        return indexes;
    }

    @Override
    public int count(Document query, int skip, int limit) {
        if (query == null || query.keySet().isEmpty()) {
            int count = count();
            if (skip > 0) {
                count = Math.max(0, count - skip);
            }
            if (limit > 0) {
                return Math.min(limit, count);
            }
            return count;
        }

        int numberToReturn = Math.max(limit, 0);
        int count = 0;
        Iterator<?> it = queryDocuments(query, null, skip, numberToReturn, 0, new Document(getIdField(), 1)).iterator();
        while (it.hasNext()) {
            it.next();
            count++;
        }
        return count;
    }

    @Override
    public Document getStats() {
        int dataSize = getDataSize();
        int count = count();

        Document response = new Document("ns", getFullName());
        response.put("count", Integer.valueOf(count));
        response.put("size", Integer.valueOf(dataSize));

        int averageSize = 0;
        if (count > 0) {
            averageSize = dataSize / count;
        }
        response.put("avgObjSize", Integer.valueOf(averageSize));
        response.put("storageSize", Integer.valueOf(0));
        response.put("numExtents", Integer.valueOf(0));
        response.put("nindexes", Integer.valueOf(indexes.size()));
        Document indexSizes = new Document();
        for (Index<P> index : indexes) {
            indexSizes.put(index.getName(), Long.valueOf(index.getDataSize()));
        }

        response.put("indexSize", indexSizes);
        Utils.markOkay(response);
        return response;
    }

    @Override
    public void removeDocument(Document document) {
        P position = null;

        if (!indexes.isEmpty()) {
            for (Index<P> index : indexes) {
                P indexPosition = index.remove(document);
                if (indexPosition == null) {
                    if (index.isSparse()) {
                        continue;
                    } else {
                        throw new IllegalStateException("Found no position for " + document + " in " + index);
                    }
                }
                if (position != null) {
                    Assert.equals(position, indexPosition, () -> "Got different positions for " + document);
                }
                position = indexPosition;
            }
        } else {
            position = findDocumentPosition(document);
        }

        if (position == null) {
            // not found
            return;
        }

        if (tracksDataSize()) {
            updateDataSize(-Utils.calculateSize(document));
        }

        removeDocument(position);
    }

    @VisibleForExternalBackends
    protected boolean tracksDataSize() {
        return true;
    }

    @Override
    public Document validate() {
        Document response = new Document("ns", getFullName());
        response.put("extentCount", Integer.valueOf(0));
        response.put("datasize", Long.valueOf(getDataSize()));
        response.put("nrecords", Integer.valueOf(count()));

        response.put("nIndexes", Integer.valueOf(indexes.size()));
        Document keysPerIndex = new Document();
        for (Index<P> index : indexes) {
            keysPerIndex.put(index.getName(), Long.valueOf(index.getCount()));
        }

        response.put("keysPerIndex", keysPerIndex);
        response.put("valid", Boolean.TRUE);
        response.put("errors", Collections.emptyList());
        Utils.markOkay(response);
        return response;
    }

    @Override
    public void renameTo(MongoDatabase newDatabase, String newCollectionName) {
        this.database = newDatabase;
        this.collectionName = newCollectionName;
    }

    protected abstract void removeDocument(P position);

    protected P findDocumentPosition(Document document) {
        return streamAllDocumentsWithPosition()
            .filter(match -> documentMatchesQuery(match.getDocument(), document))
            .map(DocumentWithPosition::getPosition)
            .findFirst()
            .orElse(null);
    }

    protected abstract Stream<DocumentWithPosition<P>> streamAllDocumentsWithPosition();

    private boolean isSystemCollection() {
        return AbstractMongoDatabase.isSystemCollection(getCollectionName());
    }

    protected QueryResult createQueryResult(List<Document> matchedDocuments, int batchSize) {
        final Collection<Document> firstBatch;
        if (batchSize > 0) {
            firstBatch = matchedDocuments.stream()
                .limit(batchSize)
                .collect(Collectors.toList());
        } else {
            firstBatch = matchedDocuments;
        }

        List<Document> remainingDocuments = matchedDocuments.subList(firstBatch.size(), matchedDocuments.size());

        if (remainingDocuments.isEmpty()) {
            return new QueryResult(firstBatch);
        } else {
            Cursor cursor = createCursor(remainingDocuments);
            return new QueryResult(firstBatch, cursor);
        }
    }

    protected Cursor createCursor(List<Document> remainingDocuments) {
        InMemoryCursor cursor = new InMemoryCursor(cursorRegistry.generateCursorId(), remainingDocuments);
        cursorRegistry.add(cursor);
        return cursor;
    }

}
<file_sep>/updateDependencies.sh
#!/bin/bash

./gradlew --refresh-dependencies dependencies mongo-java-server-{core,test-common,{h2,memory,postgres}-backend,examples}:dependencies --update-locks '*:*'
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/stage/LimitStage.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import java.util.stream.Stream;

import de.bwaldvogel.mongo.bson.Document;

public class LimitStage implements AggregationStage {

    private final long maxSize;

    public LimitStage(long maxSize) {
        this.maxSize = maxSize;
    }

    @Override
    public String name() {
        return "$limit";
    }

    @Override
    public Stream<Document> apply(Stream<Document> stream) {
        return stream.limit(maxSize);
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/DefaultQueryMatcher.java
package de.bwaldvogel.mongo.backend;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.bwaldvogel.mongo.backend.aggregation.Expression;
import de.bwaldvogel.mongo.bson.BsonRegularExpression;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.bson.MaxKey;
import de.bwaldvogel.mongo.bson.MinKey;
import de.bwaldvogel.mongo.exception.BadValueException;
import de.bwaldvogel.mongo.exception.ErrorCode;
import de.bwaldvogel.mongo.exception.FailedToParseException;
import de.bwaldvogel.mongo.exception.MongoServerError;
import de.bwaldvogel.mongo.exception.MongoServerException;
import de.bwaldvogel.mongo.exception.MongoServerNotYetImplementedException;

public class DefaultQueryMatcher implements QueryMatcher {

    private static final Logger log = LoggerFactory.getLogger(DefaultQueryMatcher.class);

    private Integer lastPosition;

    @Override
    public boolean matches(Document document, Document query) {
        for (String key : query.keySet()) {
            Object queryValue = query.get(key);
            validateQueryValue(queryValue, key);
            if (!checkMatch(queryValue, key, document)) {
                return false;
            }
        }

        return true;
    }

    private void validateQueryValue(Object queryValue, String key) {
        if (!(queryValue instanceof Document)) {
            return;
        }
        if (BsonRegularExpression.isRegularExpression(queryValue)) {
            return;
        }
        Document queryObject = (Document) queryValue;
        if (!queryObject.keySet().isEmpty() && queryObject.keySet().iterator().next().startsWith("$")) {
            for (String operator : queryObject.keySet()) {
                if (Constants.REFERENCE_KEYS.contains(operator)) {
                    continue;
                }
                QueryOperator queryOperator = QueryOperator.fromValue(operator);
                if (queryOperator == QueryOperator.TYPE) {
                    Object value = queryObject.get(operator);
                    if (value instanceof Collection) {
                        Collection<?> values = (Collection<?>) value;
                        if (values.isEmpty()) {
                            throw new FailedToParseException(key + " must match at least one type");
                        }
                    }
                }
            }
        }
    }

    @Override
    public synchronized Integer matchPosition(Document document, Document query) {
        lastPosition = null;
        for (String key : query.keySet()) {
            if (!checkMatch(query.get(key), key, document)) {
                return null;
            }
        }

        return lastPosition;
    }

    private List<String> splitKey(String key) {
        List<String> keys = List.of(key.split("\\.", -1));
        for (int i = 0; i < keys.size(); i++) {
            if (keys.get(i).isEmpty() && i != keys.size() - 1) {
                log.warn("Illegal key: '{}'", key);
                return List.of(key);
            }
        }
        return keys;
    }

    private boolean checkMatch(Object queryValue, String key, Object document) {
        return checkMatch(queryValue, splitKey(key), document);
    }

    private boolean checkMatch(Object queryValue, List<String> keys, Object value) {
        if (keys.isEmpty()) {
            throw new MongoServerException("illegal keys: " + keys);
        }

        String firstKey = keys.get(0);

        if (firstKey.equals("$comment")) {
            log.debug("query comment: '{}'", queryValue);
            return true;
        }

        List<String> subKeys = Collections.emptyList();
        if (keys.size() > 1) {
            subKeys = keys.subList(1, keys.size());
        }

        if (QueryFilter.isQueryFilter(firstKey)) {
            QueryFilter filter = QueryFilter.fromValue(firstKey);
            return checkMatch(queryValue, filter, value);
        } else if (firstKey.startsWith("$") && !Constants.REFERENCE_KEYS.contains(firstKey)) {
            throw new BadValueException("unknown top level operator: " + firstKey + ". If you have a field name that starts with a '$' symbol, consider using $getField or $setField.");
        }

        if (value instanceof List<?>) {
            if (firstKey.matches("\\d+")) {
                Object listValue = Utils.getFieldValueListSafe(value, firstKey);
                if (subKeys.isEmpty()) {
                    return checkMatchesValue(queryValue, listValue);
                } else {
                    return checkMatch(queryValue, subKeys, listValue);
                }
            } else if (firstKey.isEmpty()) {
                Assert.isEmpty(subKeys);
                return checkMatchesValue(queryValue, value);
            }

            if (queryValue instanceof Document) {
                Document query = (Document) queryValue;
                if (query.containsKey(QueryOperator.ALL.getValue())) {
                    Object allQuery = query.get(QueryOperator.ALL.getValue());
                    return checkMatchesAllDocuments(allQuery, keys, value);
                }
                if (query.containsKey(QueryOperator.NOT_EQUALS.getValue())) {
                    Object notEqualQuery = query.get(QueryOperator.NOT_EQUALS.getValue());
                    return !checkMatchesAnyDocument(notEqualQuery, keys, value);
                }
                if (query.containsKey(QueryOperator.NOT_IN.getValue())) {
                    Object notInQueryValue = query.get(QueryOperator.NOT_IN.getValue());
                    Document inQuery = new Document(QueryOperator.IN.getValue(), notInQueryValue);
                    return !checkMatchesAnyDocument(inQuery, keys, value);
                }
            }

            return checkMatchesAnyDocument(queryValue, keys, value);
        }

        if (!subKeys.isEmpty()) {
            Object subObject = Utils.getFieldValueListSafe(value, firstKey);
            return checkMatch(queryValue, subKeys, subObject);
        }

        final Document document;
        final Object documentValue;
        if (Missing.isNullOrMissing(value)) {
            document = null;
            documentValue = Missing.getInstance();
        } else if (value instanceof Document) {
            document = (Document) value;
            documentValue = document.getOrMissing(firstKey);
        } else {
            return checkMatchesValue(queryValue, Missing.getInstance());
        }

        if (documentValue instanceof Collection<?>) {
            Collection<?> documentValues = (Collection<?>) documentValue;
            if (queryValue instanceof Document) {
                Document queryDocument = (Document) queryValue;
                boolean matches = checkMatchesAnyValue(queryDocument, keys, document, documentValues);
                if (matches) {
                    return true;
                }
                if (isInQuery(queryDocument)) {
                    return checkMatchesValue(queryValue, documentValue);
                } else {
                    return false;
                }
            } else if (queryValue instanceof Collection<?>) {
                return checkMatchesValue(queryValue, documentValues);
            } else if (checkMatchesAnyValue(queryValue, documentValues)) {
                return true;
            }
        }

        return checkMatchesValue(queryValue, documentValue);
    }

    private static boolean isInQuery(Document queryDocument) {
        return queryDocument.keySet().equals(Set.of(QueryOperator.IN.getValue()));
    }

    private boolean checkMatchesAnyValue(Document queryValue, List<String> keys, Document document, Collection<?> value) {
        Set<String> keySet = queryValue.keySet();

        // clone first
        Document queryValueClone = queryValue.clone();

        for (String queryOperator : keySet) {

            Object subQuery = queryValueClone.remove(queryOperator);

            if (queryOperator.equals(QueryOperator.ALL.getValue())) {
                if (!checkMatchesAllValues(subQuery, value)) {
                    return false;
                }
            } else if (queryOperator.equals(QueryOperator.IN.getValue())) {
                Document inQuery = new Document(queryOperator, subQuery);
                if (!checkMatchesAnyValue(inQuery, value)) {
                    return false;
                }
            } else if (queryOperator.equals(QueryOperator.NOT_IN.getValue())) {
                Document inQuery = new Document(QueryOperator.IN.getValue(), subQuery);
                if (checkMatchesAnyValue(inQuery, value)) {
                    return false;
                }
            } else if (queryOperator.equals(QueryOperator.NOT.getValue())) {
                if (checkMatch(subQuery, keys, document)) {
                    return false;
                }
            } else if (queryOperator.equals(QueryOperator.NOT_EQUALS.getValue())) {
                Document equalQuery = new Document(QueryOperator.EQUAL.getValue(), subQuery);
                if (subQuery instanceof Collection) {
                    if (checkMatchesValue(subQuery, value)) {
                        return false;
                    }
                } else if (checkMatchesAnyValue(equalQuery, value)) {
                    return false;
                }
            } else if (queryOperator.equals(QueryOperator.SIZE.getValue())) {
                Document sizeQuery = new Document(QueryOperator.SIZE.getValue(), subQuery);
                if (!checkMatchesValue(sizeQuery, value)) {
                    return false;
                }
            } else {
                if (!checkMatchesAnyValue(queryValue, value) && !checkMatchesValue(queryValue, value)) {
                    return false;
                }
            }
        }
        return true;
    }

    private boolean checkMatch(Object queryValue, QueryFilter filter, Object document) {
        if (filter == QueryFilter.EXPR) {
            Object result = Expression.evaluateDocument(queryValue, (Document) document);
            return Utils.isTrue(result);
        }

        if (!(queryValue instanceof List<?>)) {
            throw new BadValueException("$and/$or/$nor must be a nonempty array");
        }

        @SuppressWarnings("unchecked")
        List<Object> list = (List<Object>) queryValue;
        if (list.isEmpty()) {
            throw new BadValueException("$and/$or/$nor must be a nonempty array");
        }

        for (Object subqueryValue : list) {
            if (!(subqueryValue instanceof Document)) {
                throw new MongoServerError(14817, filter + " elements must be objects");
            }
        }

        switch (filter) {
            case AND:
                for (Object subqueryValue : list) {
                    if (!matches((Document) document, (Document) subqueryValue)) {
                        return false;
                    }
                }
                return true;
            case OR:
                for (Object subqueryValue : list) {
                    if (matches((Document) document, (Document) subqueryValue)) {
                        return true;
                    }
                }
                return false;
            case NOR:
                return !checkMatch(queryValue, QueryFilter.OR, document);
            default:
                throw new MongoServerException("illegal query filter: " + filter + ". must not happen");
        }
    }

    @SuppressWarnings("unchecked")
    private boolean checkMatchesAllDocuments(Object queryValue, List<String> keys, Object document) {
        for (Object query : (Collection<Object>) queryValue) {
            if (!checkMatchesAnyDocument(query, keys, document)) {
                return false;
            }
        }
        return true;
    }

    @SuppressWarnings("unchecked")
    private boolean checkMatchesAnyDocument(Object queryValue, List<String> keys, Object document) {
        int i = 0;
        for (Object object : (Collection<Object>) document) {
            if (checkMatch(queryValue, keys, object)) {
                if (lastPosition == null) {
                    lastPosition = Integer.valueOf(i);
                }
                return true;
            }
            i++;
        }
        return false;
    }

    @Override
    public boolean matchesValue(Object queryValue, Object value) {
        return checkMatchesValue(queryValue, value, false);
    }

    private boolean checkMatchesValue(Object queryValue, Object value) {
        return checkMatchesValue(queryValue, value, true);
    }

    private boolean checkMatchesValue(Object queryValue, Object value, boolean requireExactMatch) {
        if (BsonRegularExpression.isRegularExpression(queryValue)) {
            if (Missing.isNullOrMissing(value)) {
                return false;
            } else {
                BsonRegularExpression pattern = BsonRegularExpression.convertToRegularExpression(queryValue);
                Matcher matcher = pattern.matcher(value.toString());
                return matcher.find();
            }
        }

        if (queryValue instanceof Document) {
            Document queryObject = (Document) queryValue;

            if (queryObject.keySet().equals(Constants.REFERENCE_KEYS)) {
                if (value instanceof Document) {
                    return matches((Document) value, queryObject);
                } else {
                    return false;
                }
            }

            if (requireExactMatch && value instanceof Document) {
                if (queryObject.keySet().stream().noneMatch(key -> key.startsWith("$"))) {
                    return Utils.nullAwareEquals(value, queryValue);
                }
            }

            for (String key : queryObject.keySet()) {
                Object querySubvalue = queryObject.get(key);
                if (key.startsWith("$")) {
                    if (!checkExpressionMatch(value, querySubvalue, key)) {
                        return false;
                    }
                } else if (Missing.isNullOrMissing(value) && querySubvalue == null) {
                    return false;
                } else if (!checkMatch(querySubvalue, key, value)) {
                    // the value of the query itself can be a complex query
                    return false;
                }
            }
            return true;
        }

        return Utils.nullAwareEquals(value, queryValue);
    }

    private boolean checkMatchesAllValues(Object queryValue, Object values) {
        if (!(queryValue instanceof Collection)) {
            return false;
        }

        Collection<?> list = (Collection<?>) values;
        Collection<?> queryValues = (Collection<?>) queryValue;

        if (queryValues.isEmpty()) {
            return false;
        }

        for (Object query : queryValues) {
            if (!checkMatchesAnyValue(query, list)) {
                return false;
            }
        }
        return true;
    }

    @SuppressWarnings("unchecked")
    private boolean checkMatchesElemValues(Object queryValue, Object values) {
        if (!(queryValue instanceof Document)) {
            throw new BadValueException(QueryOperator.ELEM_MATCH.getValue() + " needs an Object");
        }
        if (!(values instanceof Collection)) {
            return false;
        }
        Collection<Object> list = (Collection<Object>) values;
        for (Object value : list) {
            if (checkMatchesValue(queryValue, value, false)) {
                return true;
            }
        }
        return false;
    }

    private boolean checkMatchesAnyValue(Object queryValue, Collection<?> values) {
        if (queryValue instanceof Document) {
            Document queryDocument = (Document) queryValue;
            if (queryDocument.keySet().equals(Set.of(QueryOperator.ELEM_MATCH.getValue()))) {
                queryValue = queryDocument.get(QueryOperator.ELEM_MATCH.getValue());
            }
        }

        int i = 0;
        for (Object value : values) {
            if (checkMatchesValue(queryValue, value)) {
                if (lastPosition == null) {
                    lastPosition = Integer.valueOf(i);
                }
                return true;
            }
            i++;
        }
        return false;
    }

    private boolean checkExpressionMatch(Object value, Object expressionValue, String operator) {

        if (QueryFilter.isQueryFilter(operator)) {
            QueryFilter filter = QueryFilter.fromValue(operator);
            return checkMatch(expressionValue, filter, value);
        }

        QueryOperator queryOperator = QueryOperator.fromValue(operator);

        switch (queryOperator) {
            case IN:
                Collection<?> queriedObjects = (Collection<?>) expressionValue;
                for (Object o : queriedObjects) {
                    if (o instanceof BsonRegularExpression && value instanceof String) {
                        BsonRegularExpression pattern = (BsonRegularExpression) o;
                        if (pattern.matcher((String) value).find()) {
                            return true;
                        }
                    } else if (value instanceof Collection && !(o instanceof Collection)) {
                        Collection<?> values = (Collection<?>) value;
                        return values.stream().anyMatch(v -> Utils.nullAwareEquals(o, v));
                    } else if (Utils.nullAwareEquals(o, value)) {
                        return true;
                    }
                }
                return false;
            case NOT:
                return !checkMatchesValue(expressionValue, value);
            case EQUAL:
                return Utils.nullAwareEquals(value, expressionValue);
            case NOT_EQUALS:
                return !Utils.nullAwareEquals(value, expressionValue);
            case NOT_IN:
                return !checkExpressionMatch(value, expressionValue, "$in");
            case EXISTS:
                return ((value instanceof Missing) != Utils.isTrue(expressionValue));
            case GREATER_THAN:
                if (!comparableTypes(value, expressionValue)) {
                    return false;
                }
                return ValueComparator.desc().compare(value, expressionValue) < 0;
            case GREATER_THAN_OR_EQUAL:
                if (!comparableTypes(value, expressionValue)) {
                    return false;
                }
                return ValueComparator.desc().compare(value, expressionValue) <= 0;
            case LESS_THAN:
                if (!comparableTypes(value, expressionValue)) {
                    return false;
                }
                return ValueComparator.asc().compare(value, expressionValue) < 0;
            case LESS_THAN_OR_EQUAL:
                if (!comparableTypes(value, expressionValue)) {
                    return false;
                }
                return ValueComparator.asc().compare(value, expressionValue) <= 0;
            case MOD: {
                if (!(value instanceof Number)) {
                    return false;
                }

                @SuppressWarnings("unchecked")
                List<Number> modValue = (List<Number>) expressionValue;
                return (((Number) value).intValue() % modValue.get(0).intValue() == modValue.get(1).intValue());
            }
            case SIZE: {
                if (!(expressionValue instanceof Number)) {
                    throw new BadValueException("$size needs a number");
                }
                if (!(value instanceof Collection<?>)) {
                    return false;
                }
                int listSize = ((Collection<?>) value).size();
                double matchingSize = ((Number) expressionValue).doubleValue();
                return listSize == matchingSize;
            }
            case ALL:
                return false;
            case TYPE:
                return matchTypes(value, expressionValue);
            case ELEM_MATCH:
                return checkMatchesElemValues(expressionValue, value);
            case NEAR_SPHERE:
                return checkNearSphere(expressionValue, value);
            case GEO_WITHIN:
                return checkGeoWithin(expressionValue, value);
            default:
                throw new IllegalArgumentException("unhandled query operator: " + queryOperator);
        }
    }

    static boolean matchTypes(Object value, Object expressionValue) {
        if (Objects.equals(expressionValue, "number")) {
            List<String> types = Stream.of(BsonType.INT, BsonType.LONG, BsonType.DOUBLE, BsonType.DECIMAL128)
                .map(BsonType::getAlias)
                .collect(Collectors.toList());
            return matchTypes(value, types);
        } else if (expressionValue instanceof String) {
            return matchTypes(value, BsonType.forString((String) expressionValue));
        } else if (expressionValue instanceof Number) {
            return matchTypes(value, BsonType.forNumber((Number) expressionValue));
        } else if (expressionValue instanceof Collection) {
            Collection<?> values = (Collection<?>) expressionValue;
            for (Object type : values) {
                if (matchTypes(value, type)) {
                    return true;
                }
            }
            return false;
        } else {
            throw new MongoServerError(ErrorCode.TypeMismatch, "type must be represented as a number or a string");
        }
    }

    private static boolean matchTypes(Object value, BsonType type) {
        return type.matches(value);
    }

    private boolean comparableTypes(Object value, Object expressionValue) {
        if (expressionValue instanceof MinKey || expressionValue instanceof MaxKey) {
            return true;
        }
        value = Utils.normalizeValue(value);
        expressionValue = Utils.normalizeValue(expressionValue);
        if (value == null || expressionValue == null) {
            return false;
        }

        if (value instanceof Number && expressionValue instanceof Number) {
            return true;
        }

        return value.getClass().equals(expressionValue.getClass());
    }

    private boolean checkNearSphere(Object expressionValue, Object value) {
        log.debug("Expression value: {}, value: {}", expressionValue, value);
        throw new MongoServerNotYetImplementedException(132, QueryOperator.NEAR_SPHERE.getValue());
    }

    private boolean checkGeoWithin(Object expressionValue, Object value) {
        log.debug("Expression value: {}, value: {}", expressionValue, value);
        throw new MongoServerNotYetImplementedException(132, QueryOperator.GEO_WITHIN.getValue());
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/UnsupportedConversionError.java
package de.bwaldvogel.mongo.exception;

import static de.bwaldvogel.mongo.backend.Utils.describeType;

public class UnsupportedConversionError extends ConversionFailureException {

    private static final long serialVersionUID = 1L;

    public UnsupportedConversionError(Object value, Class<?> targetType) {
        super("Unsupported conversion from " + describeType(value)
            + " to " + describeType(targetType) + " in $convert with no onError value");
    }
}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/aggregation/ExpressionTest.java
package de.bwaldvogel.mongo.backend.aggregation;

import static de.bwaldvogel.mongo.TestUtils.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Test;

import de.bwaldvogel.mongo.backend.Missing;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.FailedToOptimizePipelineError;
import de.bwaldvogel.mongo.exception.MongoServerError;

class ExpressionTest {

    @Test
    void testEvaluateSimpleValue() throws Exception {
        assertThat(Expression.evaluate(1, json(""))).isEqualTo(1);
        assertThat(Expression.evaluate(null, json(""))).isNull();
        assertThat(Expression.evaluate("abc", json(""))).isEqualTo("abc");
        assertThat(Expression.evaluate("$a", json("a: 123"))).isEqualTo(123);
        assertThat(Expression.evaluate("$a", json(""))).isInstanceOf(Missing.class);
        assertThat(Expression.evaluate("$a", json("a: null"))).isNull();
        assertThat(Expression.evaluate(json("a: 1, b: 2"), json("a: -2"))).isEqualTo(json("a: 1, b: 2"));
    }

    @Test
    void testEvaluateAbs() throws Exception {
        assertThat(Expression.evaluate(json("$abs: '$a'"), json("a: -2"))).isEqualTo(2);
        assertThat(Expression.evaluate(json("$abs: '$a'"), json("a: -2.5"))).isEqualTo(2.5);
        assertThat(Expression.evaluate(json("$abs: ['$a']"), json("a: -2.5"))).isEqualTo(2.5);
        assertThat(Expression.evaluate(new Document("$abs", 123L), json(""))).isEqualTo(123);
        assertThat(Expression.evaluate(json("$abs: null"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$abs: '$a'"), json("a: -25"))).isEqualTo(25);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$abs: '$a', $ceil: '$b'"), json("")))
            .withMessage("[Error 15983] An object representing an expression must have exactly one field: {\"$abs\" : \"$a\", \"$ceil\" : \"$b\"}");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$abs: 'abc'"), json("")))
            .withMessage("[Error 28765] $abs only supports numeric types, not string");
    }

    @Test
    void testEvaluateAdd() throws Exception {
        assertThat(Expression.evaluate(json("$add: ['$a', '$b']"), json("a: 7, b: 5"))).isEqualTo(12);
        assertThat(Expression.evaluate(json("$add: ['$doesNotExist', 5]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$add: [7.5, 3]"), json(""))).isEqualTo(10.5);
        assertThat(Expression.evaluate(json("$add: [1, 2, 3]"), json(""))).isEqualTo(6);
        assertThat(Expression.evaluate(json("$add: []"), json(""))).isEqualTo(0);
        assertThat(Expression.evaluate(json("$add: 17"), json(""))).isEqualTo(17);
        assertThat(Expression.evaluate(json("$add: [1, null, 2]"), json(""))).isNull();
        assertThat(Expression.evaluate(new Document("$add",
            List.of(Instant.ofEpochSecond(1000), Instant.ofEpochSecond(2000))), json("")))
            .isEqualTo(Instant.ofEpochSecond(3000));

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$add: 'abc'"), json("")))
            .withMessage("[Error 16554] $add only supports numeric or date types, not string");
    }

    @Test
    void testEvaluateAnd() throws Exception {
        assertThat(Expression.evaluate(json("$and: [1, 'green']"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$and: []"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$and: [[null], [false], [0]]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$and: 'abc'"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$and: '$value'"), json("value: true"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$and: '$value'"), json("value: false"))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$and: true"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$and: [{$gt: ['$qty', 100]}, {$lt: ['$qty', 250]}]"), json("qty: 150"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$and: [{$gt: ['$qty', 100]}, {$lt: ['$qty', 250]}]"), json("qty: 300"))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$and: false"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$and: null"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$and: [null, true]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$and: [0, true]"), json(""))).isEqualTo(false);
    }

    @Test
    void testEvaluateAnyElementTrue() throws Exception {
        assertThat(Expression.evaluate(json("$anyElementTrue: [[true, false]]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$anyElementTrue: ['$items']"), json("items: [false, true]"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$anyElementTrue: [[[false]]]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$anyElementTrue: ['$items']"), json("items: [false, false]"))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$anyElementTrue: [[null, false, 0]]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$anyElementTrue: [[]]"), json(""))).isEqualTo(false);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$anyElementTrue: null"), json("")))
            .withMessage("[Error 17041] $anyElementTrue's argument must be an array, but is null");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$anyElementTrue: [null]"), json("")))
            .withMessage("[Error 17041] $anyElementTrue's argument must be an array, but is null");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$anyElementTrue: 'abc'"), json("")))
            .withMessage("[Error 17041] $anyElementTrue's argument must be an array, but is string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$anyElementTrue: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $anyElementTrue takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluateAllElementsTrue() throws Exception {
        assertThat(Expression.evaluate(json("$allElementsTrue: [[true, 1, 'someString']]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$allElementsTrue: [[[false]]]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$allElementsTrue: [[]]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$allElementsTrue: ['$items']"), json("items: [true]"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$allElementsTrue: [[null, false, 0]]"), json(""))).isEqualTo(false);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$allElementsTrue: null"), json("")))
            .withMessage("[Error 17040] $allElementsTrue's argument must be an array, but is null");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$allElementsTrue: [null]"), json("")))
            .withMessage("[Error 17040] $allElementsTrue's argument must be an array, but is null");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$allElementsTrue: 'abc'"), json("")))
            .withMessage("[Error 17040] $allElementsTrue's argument must be an array, but is string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$allElementsTrue: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $allElementsTrue takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluateArrayElemAt() throws Exception {
        assertThat(Expression.evaluate(json("$arrayElemAt: [[1, 2, 3], 0]"), json(""))).isEqualTo(1);
        assertThat(Expression.evaluate(json("$arrayElemAt: [[1, 2, 3], 1.0]"), json(""))).isEqualTo(2);
        assertThat(Expression.evaluate(json("$arrayElemAt: [[1, 2, 3], -2]"), json(""))).isEqualTo(2);
        assertThat(Expression.evaluate(json("$arrayElemAt: [[1, 2, 3], 15]"), json(""))).isInstanceOf(Missing.class);
        assertThat(Expression.evaluate(json("$arrayElemAt: ['$items', 1]"), json("items: ['a', 'b', 'c']"))).isEqualTo("b");
        assertThat(Expression.evaluate(json("$arrayElemAt: ['$items', '$pos']"), json("items: ['a', 'b', 'c'], pos: -1"))).isEqualTo("c");
        assertThat(Expression.evaluate(json("$arrayElemAt: ['$items', '$pos']"), json(""))).isNull();

        assertThat(Expression.evaluate(json("$arrayElemAt: ['$items.foo', -1]"), json("items: [{foo: 'bar'}, {foo: 'bas'}, {foo: 'bat'}]")))
            .isEqualTo("bat");
        assertThat(Expression.evaluate(json("$arrayElemAt: ['$items.foo', 0]"), json("items: [{foo: {ping: 'pong'}}, {foo: 'bas'}, {foo: 'bat'}]")))
            .isEqualTo(json("ping: 'pong'"));
        assertThat(Expression.evaluate(json("$arrayElemAt: ['$items.foo', 1]"), json("items: [1, {foo: 11}, 3]")))
            .isEqualTo(11);
        assertThat(Expression.evaluate(json("$arrayElemAt: ['$items.foo', 0]"), json("items: [1, {foo: 11}, 3]")))
            .isInstanceOf(Missing.class);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$arrayElemAt: null"), json("")))
            .withMessage("[Error 16020] Expression $arrayElemAt takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$arrayElemAt: [1, 2, 3]"), json("")))
            .withMessage("[Error 16020] Expression $arrayElemAt takes exactly 2 arguments. 3 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$arrayElemAt: ['a', 'b']"), json("")))
            .withMessage("[Error 28689] $arrayElemAt's first argument must be an array, but is string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$arrayElemAt: [['a', 'b'], 'b']"), json("")))
            .withMessage("[Error 28690] $arrayElemAt's second argument must be a numeric value, but is string");
    }

    @Test
    void testEvaluateArrayToObject() throws Exception {
        assertThat(Expression.evaluate(json("$arrayToObject: {$literal: [['item', 'abc123'], ['qty', 25]]}"), json("")))
            .isEqualTo(json("item: 'abc123', qty: 25"));

        assertThat(Expression.evaluate(json("$arrayToObject: {$literal: [{k: 'item', v: 'abc123'}, {k: 'qty', v: 25}]}"), json("")))
            .isEqualTo(json("item: 'abc123', qty: 25"));

        assertThat(Expression.evaluate(json("$arrayToObject: '$kv'"), json("kv: [['k', 'v']]")))
            .isEqualTo(json("k: 'v'"));

        assertThatExceptionOfType(FailedToOptimizePipelineError.class)
            .isThrownBy(() -> Expression.evaluate(json("$arrayToObject: 'str'"), json("")))
            .withMessage("[Error 40386] Failed to optimize pipeline :: caused by :: $arrayToObject requires an array input, found: string");

        assertThatExceptionOfType(FailedToOptimizePipelineError.class)
            .isThrownBy(() -> Expression.evaluate(json("$arrayToObject: '$kv'"), json("kv: [1, 2]")))
            .withMessage("[Error 40398] Failed to optimize pipeline :: caused by :: Unrecognised input type format for $arrayToObject: int");

        assertThatExceptionOfType(FailedToOptimizePipelineError.class)
            .isThrownBy(() -> Expression.evaluate(json("$arrayToObject: '$kv'"), json("kv: [[1, 2, 3]]")))
            .withMessage("[Error 40397] Failed to optimize pipeline :: caused by :: $arrayToObject requires an array of size 2 arrays,found array of size: 3");

        assertThatExceptionOfType(FailedToOptimizePipelineError.class)
            .isThrownBy(() -> Expression.evaluate(json("$arrayToObject: '$kv'"), json("kv: [[1, 2]]")))
            .withMessage("[Error 40395] Failed to optimize pipeline :: caused by :: $arrayToObject requires an array of key-value pairs, where the key must be of type string. Found key type: int");

        assertThatExceptionOfType(FailedToOptimizePipelineError.class)
            .isThrownBy(() -> Expression.evaluate(json("$arrayToObject: '$kv'"), json("kv: 1")))
            .withMessage("[Error 40386] Failed to optimize pipeline :: caused by :: $arrayToObject requires an array input, found: int");

        assertThatExceptionOfType(FailedToOptimizePipelineError.class)
            .isThrownBy(() -> Expression.evaluate(json("$arrayToObject: '$kv'"), json("kv: [{}]")))
            .withMessage("[Error 40392] Failed to optimize pipeline :: caused by :: $arrayToObject requires an object keys of 'k' and 'v'. Found incorrect number of keys:0");

        assertThatExceptionOfType(FailedToOptimizePipelineError.class)
            .isThrownBy(() -> Expression.evaluate(json("$arrayToObject: '$kv'"), json("kv: [{k: 1, v: 2}]")))
            .withMessage("[Error 40394] Failed to optimize pipeline :: caused by :: $arrayToObject requires an object with keys 'k' and 'v', where the value of 'k' must be of type string. Found type: int");

        assertThatExceptionOfType(FailedToOptimizePipelineError.class)
            .isThrownBy(() -> Expression.evaluate(json("$arrayToObject: '$kv'"), json("kv: [{k: 'key', z: 2}]")))
            .withMessage("[Error 40393] Failed to optimize pipeline :: caused by :: $arrayToObject requires an object with keys 'k' and 'v'. Missing either or both keys from: {k: \"key\", z: 2}");
    }

    @Test
    void testEvaluateCeil() throws Exception {
        assertThat(Expression.evaluate(json("$ceil: '$a'"), json("a: 2.5"))).isEqualTo(3.0);
        assertThat(Expression.evaluate(json("$ceil: 42"), json(""))).isEqualTo(42.0);
        assertThat(Expression.evaluate(json("$ceil: [5.4]"), json(""))).isEqualTo(6.0);
        assertThat(Expression.evaluate(json("$ceil: ['$a']"), json("a: 9.9"))).isEqualTo(10.0);
        assertThat(Expression.evaluate(json("$ceil: 42.3"), json(""))).isEqualTo(43.0);
        assertThat(Expression.evaluate(new Document("$ceil", (double) Long.MAX_VALUE), json(""))).isEqualTo(9.223372036854776E18);
        assertThat(Expression.evaluate(new Document("$ceil", (double) Long.MIN_VALUE), json(""))).isEqualTo(-9.223372036854776E18);
        assertThat(Expression.evaluate(json("$ceil: null"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$ceil: 'abc'"), json("")))
            .withMessage("[Error 28765] $ceil only supports numeric types, not string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$ceil: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $ceil takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluateCmp() throws Exception {
        assertThat(Expression.evaluate(json("$cmp: [20, 10]"), json(""))).isEqualTo(1);
        assertThat(Expression.evaluate(json("$cmp: [20, 20]"), json(""))).isEqualTo(0);
        assertThat(Expression.evaluate(json("$cmp: [10, 20]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$cmp: ['$a', '$b']"), json("a: 10, b: 5"))).isEqualTo(1);
        assertThat(Expression.evaluate(json("$cmp: ['b', 'a']"), json(""))).isEqualTo(1);
        assertThat(Expression.evaluate(json("$cmp: ['a', 'b']"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$cmp: ['$qty', 250]"), json("qty: 500"))).isEqualTo(1);
        assertThat(Expression.evaluate(json("$cmp: ['$qty', 250]"), json("qty: 100"))).isEqualTo(-1);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$cmp: 'abc'"), json("")))
            .withMessage("[Error 16020] Expression $cmp takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$cmp: ['a', 'b', 'c']"), json("")))
            .withMessage("[Error 16020] Expression $cmp takes exactly 2 arguments. 3 were passed in.");
    }

    @Test
    void testEvaluateConcat() throws Exception {
        assertThat(Expression.evaluate(json("$concat: null"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$concat: ['A', 'B', 'C']"), json(""))).isEqualTo("ABC");
        assertThat(Expression.evaluate(json("$concat: ['$a', '-', '$b']"), json("a: 'A', b: 'B'"))).isEqualTo("A-B");
        assertThat(Expression.evaluate(json("$concat: ['$a', '$b']"), json("b: 'B'"))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$concat: 1"), json("")))
            .withMessage("[Error 16702] $concat only supports strings, not int");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$concat: '$a'"), json("a: ['abc', 'def']")))
            .withMessage("[Error 16702] $concat only supports strings, not array");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$concat: [1]"), json("")))
            .withMessage("[Error 16702] $concat only supports strings, not int");
    }

    @Test
    void testEvaluateConcatArrays() throws Exception {
        assertThat(Expression.evaluate(json("$concatArrays: null"), json(""))).isNull();

        assertThat(Expression.evaluate(json("$concatArrays: [['hello', ' '], ['world']]"), json("")))
            .isEqualTo(List.of("hello", " ", "world"));

        assertThat(Expression.evaluate(json("$concatArrays: [['hello', ' '], [['world'], 'again']]"), json("")))
            .isEqualTo(List.of("hello", " ", List.of("world"), "again"));

        assertThat(Expression.evaluate(json("$concatArrays: ['$a', '$b']"), json("a: [1, 2], b: [3, 4]")))
            .isEqualTo(List.of(1, 2, 3, 4));

        assertThat(Expression.evaluate(json("$concatArrays: ['$a', '$b']"), json("a: [1, 2]")))
            .isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$concatArrays: 1"), json("")))
            .withMessage("[Error 28664] $concatArrays only supports arrays, not int");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$concatArrays: [1]"), json("")))
            .withMessage("[Error 28664] $concatArrays only supports arrays, not int");
    }

    @Test
    void testEvaluateCond() throws Exception {
        assertThat(Expression.evaluate(json("$cond: {if: {$gte: ['$qty', 250]}, then: 30, else: 20}"), json("qty: 100")))
            .isEqualTo(20);

        assertThat(Expression.evaluate(json("$cond: [{$gte: ['$qty', 250]}, 30, 20]"), json("qty: 300")))
            .isEqualTo(30);

        assertThat(Expression.evaluate(json("$cond: {if: {$gte: ['$qty', 250]}, then: '$qty', else: 20}"), json("qty: 300")))
            .isEqualTo(300);

        assertThat(Expression.evaluate(json("$cond: {if: {$gte: ['$qty', 250]}, then: 10, else: '$qty'}"), json("qty: 200")))
            .isEqualTo(200);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$cond: null"), json("")))
            .withMessage("[Error 16020] Expression $cond takes exactly 3 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$cond: [1, 2, 3, 4]"), json("")))
            .withMessage("[Error 16020] Expression $cond takes exactly 3 arguments. 4 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$cond: {}"), json("")))
            .withMessage("[Error 17080] Missing 'if' parameter to $cond");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$cond: {then: 1, else: 1}"), json("")))
            .withMessage("[Error 17080] Missing 'if' parameter to $cond");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$cond: {if: 1, else: 1}"), json("")))
            .withMessage("[Error 17080] Missing 'then' parameter to $cond");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$cond: {if: 1, then: 1}"), json("")))
            .withMessage("[Error 17080] Missing 'else' parameter to $cond");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$cond: {if: 1, then: 1, else: 1, foo: 1}"), json("")))
            .withMessage("[Error 17083] Unrecognized parameter to $cond: foo");
    }

    @Test
    void testEvaluateEq() throws Exception {
        assertThat(Expression.evaluate(json("$eq: [20, 20]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$eq: [20, 10]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$eq: [null, null]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$eq: ['$a', '$b']"), json("a: 10, b: 10"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$eq: ['$qty', 250]"), json("qty: 250"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$eq: ['$qty', 250]"), json("qty: 100"))).isEqualTo(false);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$eq: 'abc'"), json("")))
            .withMessage("[Error 16020] Expression $eq takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$eq: ['a', 'b', 'c']"), json("")))
            .withMessage("[Error 16020] Expression $eq takes exactly 2 arguments. 3 were passed in.");
    }

    @Test
    void testEvaluateMap() throws Exception {
        assertThat((Collection<Object>) Expression.evaluate(
            json("$map: {input: '$quizzes', as: 'grade', in: {$add: ['$$grade', 2]}}"),
            json("quizzes: [5, 6, 7]")))
            .containsExactly(7, 8, 9);

        assertThat((Collection<Object>) Expression.evaluate(
            json("$map: {input: '$quizzes', as: 'grade', in: {$add: ['$$grade', 2]}}"),
            json("quizzes: []")))
            .isEmpty();

        assertThat(Expression.evaluate(json("$map: {input: '$q', in: '$this'}"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$map: 'a'"), json("")))
            .withMessage("[Error 16878] $map only supports an object as its argument");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Expression.evaluate(json("$map: {input: [1, 2, 3], in: true}"), json("$this: 1")))
            .withMessage("Document already contains '$this'");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$map: {input: 'a', in: null}"), json("")))
            .withMessage("[Error 16883] input to $map must be an array not string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$map: {}"), json("")))
            .withMessage("[Error 16882] Missing 'input' parameter to $map");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$map: {input: null}"), json("")))
            .withMessage("[Error 16882] Missing 'in' parameter to $map");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$map: {input: 1, in: 1, foo: 1}"), json("")))
            .withMessage("[Error 16879] Unrecognized parameter to $map: foo");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$map: {input: [], as: [], in: 1}"), json("")))
            .withMessage("[Error 16866] empty variable names are not allowed");
        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$map: {input: [], as: '', in: 1}"), json("")))
            .withMessage("[Error 16866] empty variable names are not allowed");
    }

    @Test
    void testEvaluateReduce() throws Exception {
        assertThat((Integer) Expression.evaluate(
            json("$reduce: {input: '$quizzes', initialValue: 0, in: {$add: ['$$this', '$$value']}}"),
            json("quizzes: [5, 6, 7]")))
            .isEqualTo(18);

        final Document expectedDocument = new Document();
        expectedDocument.put("sum", 15);
        expectedDocument.put("product", 48);
        assertThat(Expression.evaluate(
            json("$reduce: {input: '$quizzes', initialValue: { sum: 5, product: 2 }, in: {sum: {$add : ['$$value.sum', '$$this']},product: {$multiply: [ '$$value.product', '$$this' ]}}}"),
            json("quizzes: [ 1, 2, 3, 4 ]")))
            .isEqualTo(expectedDocument);

        assertThat((Collection<Object>) Expression.evaluate(
            json("$reduce: {input: '$quizzes',initialValue: [ 1, 2 ],in: {$concatArrays : ['$$value', '$$this']}}"),
            json("quizzes: [ [ 3, 4 ], [ 5, 6 ] ]")))
            .containsExactly(1, 2, 3, 4, 5, 6);

        assertThat((Collection<Object>) Expression.evaluate(
            json("$reduce: {input: '$quizzes',initialValue: [],in: {$concatArrays : ['$$value', '$$this']}}"),
            json("quizzes: []")))
            .isEmpty();

        assertThat((Collection<Object>) Expression.evaluate(
            json("$reduce: {input: '$quizzes', initialValue: [ 1, 2 ], in: '$$this'}"),
            json("")))
            .isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$reduce: 'a'"), json("")))
            .withMessage("[Error 40075] $reduce only supports an object as its argument");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Expression.evaluate(json("$reduce: {input: [1, 2, 3], initialValue: null, in: '$this'}"), json("$this: 1")))
            .withMessage("Document already contains '$this'");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Expression.evaluate(json("$reduce: {input: [1, 2, 3], initialValue: null, in: '$value'}"), json("$value: 1")))
            .withMessage("Document already contains '$value'");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$reduce: {input: 'a', initialValue: null, in: null}"), json("")))
            .withMessage("[Error 40080] input to $reduce must be an array not string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$reduce: {initialValue: null, in: '$value'}"), json("")))
            .withMessage("[Error 40079] Missing 'input' parameter to $reduce");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$reduce: {input: [1, 2, 3], in: '$value'}"), json("")))
            .withMessage("[Error 40079] Missing 'initialValue' parameter to $reduce");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$reduce: {input: [1, 2, 3], initialValue: null}"), json("")))
            .withMessage("[Error 40079] Missing 'in' parameter to $reduce");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$reduce: {input: [1, 2, 3], initialValue: null, in: '$value', foo: 1}"), json("")))
            .withMessage("[Error 40076] Unrecognized parameter to $reduce: foo");
    }

    @Test
    void testEvaluateMergeObjects() throws Exception {
        assertThat(Expression.evaluate(json("$mergeObjects: [{a: 1}, null]"), json(""))).isEqualTo(json("a: 1"));
        assertThat(Expression.evaluate(json("$mergeObjects: [null, null]"), json(""))).isEqualTo(json(""));
        assertThat(Expression.evaluate(json("$mergeObjects: ['$a', '$b']"), json(""))).isEqualTo(json(""));
        assertThat(Expression.evaluate(json("$mergeObjects: ['$a', '$b']"), json("a: {x: 1}, b: {y: 2}"))).isEqualTo(json("x: 1, y: 2"));
        assertThat(Expression.evaluate(json("$mergeObjects: ['$a']"), json("a: {x: 1, y: 2}"))).isEqualTo(json("x: 1, y: 2"));
        assertThat(Expression.evaluate(json("$mergeObjects: ['$a', '$a.x']"), json("a: {x: {y: 2}}"))).isEqualTo(json("x: {y: 2}, y: 2"));

        assertThat(Expression.evaluate(json("$mergeObjects: [{a: 1}, {a: 2, b: 2}, {a: 3, c: 3}]"), json("")))
            .isEqualTo(json("a: 3, b: 2, c: 3"));

        assertThat(Expression.evaluate(json("$mergeObjects: [{a: 1}, {a: 2, b: 2}, {a: 3, b: null, c: 3}]"), json("")))
            .isEqualTo(json("a: 3, b: null, c: 3"));

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$mergeObjects: [[], []]"), json("")))
            .withMessage("[Error 40400] $mergeObjects requires object inputs, but input [] is of type array");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$mergeObjects: 'x'"), json("")))
            .withMessage("[Error 40400] $mergeObjects requires object inputs, but input \"x\" is of type string");
    }

    @Test
    void testEvaluateMinute() throws Exception {
        assertThat(Expression.evaluate(json("$minute: '$a'"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$minute: '$a'"), new Document("a", toDate("2018-07-03T14:10:00Z")))).isEqualTo(10);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$minute: '$a'"), json("a: 'abc'")))
            .withMessage("[Error 16006] can't convert from string to Date");
    }

    @Test
    void testEvaluateMod() throws Exception {
        assertThat(Expression.evaluate(json("$mod: [10, 2]"), json(""))).isEqualTo(0.0);
        assertThat(Expression.evaluate(json("$mod: [3, 2]"), json(""))).isEqualTo(1.0);
        assertThat(Expression.evaluate(json("$mod: [3.5, 3]"), json(""))).isEqualTo(0.5);
        assertThat(Expression.evaluate(json("$mod: ['$a', '$b']"), json("a: -10, b: 4"))).isEqualTo(-2.0);
        assertThat(Expression.evaluate(json("$mod: ['$a', '$b']"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$mod: [null, 2]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$mod: [2, null]"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$mod: ''"), json("")))
            .withMessage("[Error 16020] Expression $mod takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$mod: [1, 2, 3]"), json("")))
            .withMessage("[Error 16020] Expression $mod takes exactly 2 arguments. 3 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$mod: ['a', 'b']"), json("")))
            .withMessage("[Error 16611] $mod only supports numeric types, not string and string");
    }

    @Test
    void testEvaluateMonth() throws Exception {
        assertThat(Expression.evaluate(json("$month: '$a'"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$month: '$a'"), new Document("a", toDate("2018-07-03T14:00:00Z")))).isEqualTo(7);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$month: '$a'"), json("a: 'abc'")))
            .withMessage("[Error 16006] can't convert from string to Date");
    }

    @Test
    void testEvaluateMultiply() throws Exception {
        assertThat(Expression.evaluate(json("$multiply: ['$a', '$b']"), json("a: 8, b: 4"))).isEqualTo(32);
        assertThat(Expression.evaluate(json("$multiply: [4.5, 3]"), json(""))).isEqualTo(13.5);
        assertThat(Expression.evaluate(json("$multiply: [5, 3.0]"), json(""))).isEqualTo(15.0);
        assertThat(Expression.evaluate(json("$multiply: [null, 3]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$multiply: [null, null]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$multiply: [3, null]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$multiply: [3, 0]"), json(""))).isEqualTo(0);
        assertThat(Expression.evaluate(json("$multiply: [5000, 9000]"), json(""))).isEqualTo(45000000);
        assertThat(Expression.evaluate(json("$multiply: [50000, 90000]"), json(""))).isEqualTo(4500000000L);
        assertThat(Expression.evaluate(json("$multiply: [45000000000, 2]"), json(""))).isEqualTo(90000000000L);
        assertThat(Expression.evaluate(json("$multiply: [-45000000000, 2]"), json(""))).isEqualTo(-90000000000L);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$multiply: []"), json("")))
            .withMessage("[Error 16020] Expression $multiply takes exactly 2 arguments. 0 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$multiply: [1]"), json("")))
            .withMessage("[Error 16020] Expression $multiply takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$multiply: 123"), json("")))
            .withMessage("[Error 16020] Expression $multiply takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$multiply: ['a', 'b']"), json("")))
            .withMessage("[Error 16555] $multiply only supports numeric types, not string and string");
    }

    @Test
    void testEvaluateNe() throws Exception {
        assertThat(Expression.evaluate(json("$ne: [20, 20]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$ne: [20, 10]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$ne: [20, 'a']"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$ne: ['$a', '$b']"), json("a: 10, b: 10"))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$ne: ['$qty', 250]"), json("qty: 250"))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$ne: ['$qty', 250]"), json("qty: 100"))).isEqualTo(true);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$ne: 'abc'"), json("")))
            .withMessage("[Error 16020] Expression $ne takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$ne: ['a', 'b', 'c']"), json("")))
            .withMessage("[Error 16020] Expression $ne takes exactly 2 arguments. 3 were passed in.");
    }

    @Test
    void testEvaluateNot() throws Exception {
        assertThat(Expression.evaluate(json("$not: false"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$not: true"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$not: 1"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$not: 0"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$not: [true]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$not: [[false]]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$not: [false]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$not: [null]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$not: [0]"), json(""))).isEqualTo(true);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$not: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $not takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluateOr() throws Exception {
        assertThat(Expression.evaluate(json("$or: [1, 'green']"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$or: []"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$or: [[null], [false], [0]]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$or: 'abc'"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$or: '$value'"), json("value: true"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$or: '$value'"), json("value: false"))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$or: true"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$or: [{$gt: ['$qty', 100]}, {$lt: ['$qty', 250]}]"), json("qty: 150"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$or: [{$gt: ['$qty', 100]}, {$lt: ['$qty', 250]}]"), json("qty: 300"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$or: [{$gt: ['$qty', 400]}, {$lt: ['$qty', 100]}]"), json("qty: 300"))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$or: false"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$or: null"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$or: [null, true]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$or: [0, true]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$or: [0, false]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$or: [0, 0]"), json(""))).isEqualTo(false);
    }

    @Test
    void testEvaluateObjectToArray() throws Exception {
        assertThat((List<Document>) Expression.evaluate(json("$objectToArray: '$v'"), json("v: {a: 1, b: 2}")))
            .containsExactly(
                json("{k: 'a', v: 1}"),
                json("{k: 'b', v: 2}")
            );

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$objectToArray: 1"), json("")))
            .withMessage("[Error 40390] $objectToArray requires a document input, found: int");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$objectToArray: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $objectToArray takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluatePow() throws Exception {
        assertThat(Expression.evaluate(json("$pow: ['$a', '$b']"), json("a: 8, b: 4"))).isEqualTo(4096.0);
        assertThat(Expression.evaluate(json("$pow: [4.5, 3]"), json(""))).isEqualTo(91.125);
        assertThat(Expression.evaluate(json("$pow: [null, 3]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$pow: [null, null]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$pow: [3, null]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$pow: [3, 0]"), json(""))).isEqualTo(1.0);
        assertThat(Expression.evaluate(json("$pow: [-5, 0.5]"), json(""))).isEqualTo(Double.NaN);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$pow: []"), json("")))
            .withMessage("[Error 16020] Expression $pow takes exactly 2 arguments. 0 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$pow: [1]"), json("")))
            .withMessage("[Error 16020] Expression $pow takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$pow: 123"), json("")))
            .withMessage("[Error 16020] Expression $pow takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$pow: ['a', 3]"), json("")))
            .withMessage("[Error 28762] $pow's base must be numeric, not string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$pow: [3, 'a']"), json("")))
            .withMessage("[Error 28763] $pow's exponent must be numeric, not string");
    }

    @Test
    void testEvaluateRange() throws Exception {
        assertThat(Expression.evaluate(json("$range: [0, 5]"), json(""))).isEqualTo(List.of(0, 1, 2, 3, 4));
        assertThat(Expression.evaluate(json("$range: [0, 10, 2]"), json(""))).isEqualTo(List.of(0, 2, 4, 6, 8));
        assertThat(Expression.evaluate(json("$range: [0, 1.0, 2]"), json(""))).isEqualTo(List.of(0));
        assertThat(Expression.evaluate(json("$range: [0, 0, 1]"), json(""))).isEqualTo(Collections.emptyList());
        assertThat(Expression.evaluate(json("$range: [10, 0, -2]"), json(""))).isEqualTo(List.of(10, 8, 6, 4, 2));
        assertThat(Expression.evaluate(json("$range: [0, 10, -2]"), json(""))).isEqualTo(Collections.emptyList());

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$range: 'abc'"), json("")))
            .withMessage("[Error 28667] Expression $range takes at least 2 arguments, and at most 3, but 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$range: ['a', 'b']"), json("")))
            .withMessage("[Error 34443] $range requires a numeric starting value, found value of type: string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$range: ['a', 'b', 'c']"), json("")))
            .withMessage("[Error 34443] $range requires a numeric starting value, found value of type: string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$range: [0, 'b', 'c']"), json("")))
            .withMessage("[Error 34445] $range requires a numeric ending value, found value of type: string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$range: [0, 0, 'c']"), json("")))
            .withMessage("[Error 34447] $range requires a numeric step value, found value of type: string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$range: [0, 0, 0]"), json("")))
            .withMessage("[Error 34449] $range requires a non-zero step value");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$range: [0.5, 0, 1]"), json("")))
            .withMessage("[Error 34444] $range requires a starting value that can be represented as a 32-bit integer, found value: 0.5");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$range: [0, 1.5, 1]"), json("")))
            .withMessage("[Error 34446] $range requires a ending value that can be represented as a 32-bit integer, found value: 1.5");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$range: [0, 10, 0.5]"), json("")))
            .withMessage("[Error 34448] $range requires a step value that can be represented as a 32-bit integer, found value: 0.5");
    }

    @Test
    void testEvaluateReverseArray() throws Exception {
        assertThat(Expression.evaluate(json("$reverseArray: null"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$reverseArray: '$a'"), json(""))).isNull();

        assertThat(Expression.evaluate(json("$reverseArray: [[1, 2, 3]]"), json("")))
            .isEqualTo(List.of(3, 2, 1));

        assertThat(Expression.evaluate(json("$reverseArray: '$a'"), json("a: ['foo', 'bar']")))
            .isEqualTo(List.of("bar", "foo"));

        assertThat(Expression.evaluate(json("$reverseArray: ['$a']"), json("a: ['foo', 'bar']")))
            .isEqualTo(List.of("bar", "foo"));

        assertThat(Expression.evaluate(json("$reverseArray: [[]]"), json("")))
            .isEqualTo(Collections.emptyList());

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$reverseArray: 1"), json("")))
            .withMessage("[Error 34435] The argument to $reverseArray must be an array, but was of type: int");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$reverseArray: [1]"), json("")))
            .withMessage("[Error 34435] The argument to $reverseArray must be an array, but was of type: int");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$reverseArray: [[1, 2], [3, 4]]"), json("")))
            .withMessage("[Error 16020] Expression $reverseArray takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluateGt() throws Exception {
        assertThat(Expression.evaluate(json("$gt: [20, 10]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$gt: [20, 20]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$gt: ['$a', '$b']"), json("a: 10, b: 5"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$gt: ['b', 'a']"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$gt: ['a', 'b']"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$gt: ['$qty', 250]"), json("qty: 500"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$gt: ['$qty', 250]"), json("qty: 100"))).isEqualTo(false);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$gt: 'abc'"), json("")))
            .withMessage("[Error 16020] Expression $gt takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$gt: ['a', 'b', 'c']"), json("")))
            .withMessage("[Error 16020] Expression $gt takes exactly 2 arguments. 3 were passed in.");
    }

    @Test
    void testEvaluateGte() throws Exception {
        assertThat(Expression.evaluate(json("$gte: [20, 10]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$gte: [20, 20]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$gte: [20, 21]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$gte: ['$qty', 250]"), json("qty: 500"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$gte: ['$qty', 250]"), json("qty: 100"))).isEqualTo(false);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$gte: 'abc'"), json("")))
            .withMessage("[Error 16020] Expression $gte takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$gte: ['a', 'b', 'c']"), json("")))
            .withMessage("[Error 16020] Expression $gte takes exactly 2 arguments. 3 were passed in.");
    }

    @Test
    void testEvaluateHour() throws Exception {
        assertThat(Expression.evaluate(json("$hour: '$a'"), json(""))).isNull();
        int expectedHour = ZonedDateTime.ofInstant(Instant.parse("2018-07-03T14:10:00Z"), ZoneId.systemDefault()).toLocalTime().getHour();
        assertThat(Expression.evaluate(json("$hour: '$a'"), new Document("a", toDate("2018-07-03T14:10:00Z")))).isEqualTo(expectedHour);
        assertThat(Expression.evaluate(json("$hour: {date: '$a'}"), new Document("a", toDate("2018-07-03T14:10:00Z")))).isEqualTo(expectedHour);
        assertThat(Expression.evaluate(json("$hour: {date: '$a', timezone: 'UTC'}"), new Document("a", toDate("2018-07-03T14:10:00Z")))).isEqualTo(14);
        assertThat(Expression.evaluate(json("$hour: {date: '$a', timezone: '$TZ'}"), new Document("a", toDate("2018-07-03T14:10:00Z")).append("TZ", "Europe/Berlin"))).isEqualTo(16);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$hour: '$a'"), json("a: 'abc'")))
            .withMessage("[Error 16006] can't convert from string to Date");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$hour: {}"), json("")))
            .withMessage("[Error 40539] missing 'date' argument to $hour, provided: {}");
    }

    @Test
    void testEvaluateLt() throws Exception {
        assertThat(Expression.evaluate(json("$lt: [10, 20]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$lt: [20, 20]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$lt: ['$qty', 250]"), json("qty: 100"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$lt: ['$qty', 250]"), json("qty: 500"))).isEqualTo(false);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$lt: 'abc'"), json("")))
            .withMessage("[Error 16020] Expression $lt takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$lt: ['a', 'b', 'c']"), json("")))
            .withMessage("[Error 16020] Expression $lt takes exactly 2 arguments. 3 were passed in.");
    }

    @Test
    void testEvaluateLte() throws Exception {
        assertThat(Expression.evaluate(json("$lte: [10, 20]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$lte: [20, 20]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$lte: [21, 20]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$lte: ['$qty', 250]"), json("qty: 100"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$lte: ['$qty', 250]"), json("qty: 500"))).isEqualTo(false);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$lte: 'abc'"), json("")))
            .withMessage("[Error 16020] Expression $lte takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$lte: ['a', 'b', 'c']"), json("")))
            .withMessage("[Error 16020] Expression $lte takes exactly 2 arguments. 3 were passed in.");
    }

    @Test
    void testEvaluateSecond() throws Exception {
        assertThat(Expression.evaluate(json("$second: '$a'"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$second: '$a'"), new Document("a", toDate("2018-07-03T14:10:23Z")))).isEqualTo(23);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$second: '$a'"), json("a: 'abc'")))
            .withMessage("[Error 16006] can't convert from string to Date");
    }

    @Test
    void testEvaluateSetDifference() throws Exception {
        assertThat(Expression.evaluate(json("$setDifference: [null, null]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$setDifference: [[], null]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$setDifference: [null, []]"), json(""))).isNull();

        assertThat((Collection<Object>) Expression.evaluate(json("$setDifference: [['a', 'b', 'a'], ['b']]"), json("")))
            .containsExactly("a");

        assertThat((Collection<Object>) Expression.evaluate(json("$setDifference: [['a', 'b', 'a'], ['c', 'b', 'a']]"), json("")))
            .isEmpty();

        assertThat((Collection<Object>) Expression.evaluate(json("$setDifference: [['a', 'b'], [['a', 'b']]]"), json("")))
            .containsExactly("a", "b");

        assertThat((Collection<Object>) Expression.evaluate(json("$setDifference: [[1.0, 0, 2], [-0.0]]"), json("")))
            .containsExactly(1.0, 2);

        assertThat((Collection<Object>) Expression.evaluate(json("$setDifference: [[1.0, -0.0, 2], [1, 0]]"), json("")))
            .containsExactly(2);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setDifference: '$a'"), json("a: 'abc'")))
            .withMessage("[Error 16020] Expression $setDifference takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setDifference: [1, 2, 3]"), json("")))
            .withMessage("[Error 16020] Expression $setDifference takes exactly 2 arguments. 3 were passed in.");
    }

    @Test
    void testEvaluateSetEquals() throws Exception {
        assertThat(Expression.evaluate(json("$setEquals: [['a', 'b', 'a'], ['b']]"), json("")))
            .isEqualTo(false);

        assertThat(Expression.evaluate(json("$setEquals: [['a', 'b', 'a'], ['b', 'a']]"), json("")))
            .isEqualTo(true);

        assertThat(Expression.evaluate(json("$setEquals: ['$one', '$other']"), json("one: [1, 2], other: [2, 1]")))
            .isEqualTo(true);

        assertThat(Expression.evaluate(json("$setEquals: ['$one', '$other', [2]]"), json("one: [1, 2], other: [2, 1]")))
            .isEqualTo(false);

        assertThat(Expression.evaluate(json("$setEquals: ['$one', '$other', [2, 2, 1]]"), json("one: [1, 2], other: [2, 1]")))
            .isEqualTo(true);

        assertThat(Expression.evaluate(json("$setEquals: ['$one', '$other']"), json("one: [0, 2.0], other: [2, -0.0]")))
            .isEqualTo(true);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setEquals: []"), json("")))
            .withMessage("[Error 17045] $setEquals needs at least two arguments had: 0");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setEquals: [null]"), json("")))
            .withMessage("[Error 17045] $setEquals needs at least two arguments had: 1");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setEquals: null"), json("")))
            .withMessage("[Error 17045] $setEquals needs at least two arguments had: 1");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setEquals: [[]]"), json("")))
            .withMessage("[Error 17045] $setEquals needs at least two arguments had: 1");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setEquals: [1, 2]"), json("")))
            .withMessage("[Error 17044] All operands of $setEquals must be arrays. One argument is of type: int");
    }

    @Test
    void testEvaluateSetIntersection() throws Exception {
        assertThat(Expression.evaluate(json("$setIntersection: null"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$setIntersection: '$field'"), json(""))).isNull();
        assertThat((Collection<?>) Expression.evaluate(json("$setIntersection: []"), json(""))).isEmpty();
        assertThat((Collection<?>) Expression.evaluate(json("$setIntersection: [[]]"), json(""))).isEmpty();
        assertThat(Expression.evaluate(json("$setIntersection: [null]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$setIntersection: [['a'], null]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$setIntersection: ['$a', null]"), json(""))).isNull();

        assertThat((Collection<Object>) Expression.evaluate(json("$setIntersection: [['a', 'b', 'a'], ['b']]"), json("")))
            .containsExactly("b");

        assertThat((Collection<Object>) Expression.evaluate(json("$setIntersection: [['a', 'b', 'a'], ['b', 'a']]"), json("")))
            .containsExactly("a", "b");

        assertThat((Collection<Object>) Expression.evaluate(json("$setIntersection: ['$one', '$other']"), json("one: [1, 2, 3], other: [2, 3, 5]")))
            .containsExactly(2, 3);

        assertThat((Collection<Object>) Expression.evaluate(json("$setIntersection: ['$one', '$other', [2]]"), json("one: [1, 2], other: [2, 1]")))
            .containsExactly(2);

        assertThat((Collection<Object>) Expression.evaluate(json("$setIntersection: ['$one', '$other']"), json("one: [1, 2.0], other: [2, 1]")))
            .containsExactly(1, 2.0);

        assertThat((Collection<Object>) Expression.evaluate(json("$setIntersection: ['$one', '$other', [2, 2, 1]]"), json("one: [], other: [2, 1]")))
            .isEmpty();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setIntersection: [1, 2]"), json("")))
            .withMessage("[Error 17047] All operands of $setIntersection must be arrays. One argument is of type: int");
    }

    @Test
    void testEvaluateSetIsSubset() throws Exception {
        assertThat(Expression.evaluate(json("$setIsSubset: [['a', 'b', 'a'], ['b', 'a']]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$setIsSubset: [['a', 'b'], [['a', 'b']]]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$setIsSubset: ['$a', '$b']"), json("a: [1, 2], b: [1, 2, 3, 4]"))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$setIsSubset: ['$a', '$b']"), json("a: [1.0, 2.0], b: [1, 2, 3, 4]"))).isEqualTo(true);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setIsSubset: null"), json("")))
            .withMessage("[Error 16020] Expression $setIsSubset takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setIsSubset: [null]"), json("")))
            .withMessage("[Error 16020] Expression $setIsSubset takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setIsSubset: [null, []]"), json("")))
            .withMessage("[Error 17046] both operands of $setIsSubset must be arrays. First argument is of type: null");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setIsSubset: ['$doestNotExist', []]"), json("")))
            .withMessage("[Error 17046] both operands of $setIsSubset must be arrays. First argument is of type: missing");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setIsSubset: [[], '$doestNotExist']"), json("")))
            .withMessage("[Error 17042] both operands of $setIsSubset must be arrays. Second argument is of type: missing");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setIsSubset: [[], null]"), json("")))
            .withMessage("[Error 17042] both operands of $setIsSubset must be arrays. Second argument is of type: null");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setIsSubset: [1, 2]"), json("")))
            .withMessage("[Error 17046] both operands of $setIsSubset must be arrays. First argument is of type: int");
    }

    @Test
    void testEvaluateSetUnion() throws Exception {
        assertThat(Expression.evaluate(json("$setUnion: null"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$setUnion: '$a'"), json(""))).isNull();

        assertThat((Collection<Object>) Expression.evaluate(json("$setUnion: [['a', 1], ['c', 'a']]"), json("")))
            .containsExactly(1, "a", "c");

        assertThat((Collection<Object>) Expression.evaluate(json("$setUnion: ['$a', '$b']"), json("a: [1, 2, 3], b: [3.0, 4]")))
            .containsExactly(1, 2, 3, 4);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setUnion: 1"), json("")))
            .withMessage("[Error 17043] All operands of $setUnion must be arrays. One argument is of type: int");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$setUnion: [1]"), json("")))
            .withMessage("[Error 17043] All operands of $setUnion must be arrays. One argument is of type: int");
    }

    @Test
    void testEvaluateSize() throws Exception {
        assertThat(Expression.evaluate(json("$size: [['$a', '$b']]"), json("a: 7, b: 5"))).isEqualTo(2);
        assertThat(Expression.evaluate(json("$size: [[7.5, 3]]"), json(""))).isEqualTo(2);
        assertThat(Expression.evaluate(json("$size: {$literal: [7.5, 3]}"), json(""))).isEqualTo(2);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$size: null"), json("")))
            .withMessage("[Error 17124] The argument to $size must be an array, but was of type: null");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$size: 'abc'"), json("")))
            .withMessage("[Error 17124] The argument to $size must be an array, but was of type: string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$size: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $size takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluateSlice() throws Exception {
        assertThat(Expression.evaluate(json("$slice: [[1, 2, 3], 1, 1]"), json("")))
            .isEqualTo(List.of(2));

        assertThat(Expression.evaluate(json("$slice: [[1, 2, 3], 0]"), json("")))
            .isEqualTo(Collections.emptyList());

        assertThat(Expression.evaluate(json("$slice: [[1, 2, 3], 2]"), json("")))
            .isEqualTo(List.of(1, 2));

        assertThat(Expression.evaluate(json("$slice: [[1, 2, 3], 20]"), json("")))
            .isEqualTo(List.of(1, 2, 3));

        assertThat(Expression.evaluate(json("$slice: [[1, 2, 3], -2]"), json("")))
            .isEqualTo(List.of(2, 3));

        assertThat(Expression.evaluate(json("$slice: [[1, 2, 3], -20]"), json("")))
            .isEqualTo(List.of(1, 2, 3));

        assertThat(Expression.evaluate(json("$slice: [[1, 2, 3], 0, 10]"), json("")))
            .isEqualTo(List.of(1, 2, 3));

        assertThat(Expression.evaluate(json("$slice: [[1, 2, 3], 15, 2]"), json("")))
            .isEqualTo(Collections.emptyList());

        assertThat(Expression.evaluate(json("$slice: [[1, 2, 3], -15, 2]"), json("")))
            .isEqualTo(List.of(1, 2));

        assertThat(Expression.evaluate(json("$slice: [null, 0]"), json(""))).isNull();

        assertThat(Expression.evaluate(json("$slice: ['$a', 0]"), json(""))).isNull();

        assertThat(Expression.evaluate(json("$slice: ['$a', '$b', '$c']"), json("a: [1, 2, 3], b: 1, c: 1")))
            .isEqualTo(List.of(2));

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$slice: 'abc'"), json("")))
            .withMessage("[Error 28667] Expression $slice takes at least 2 arguments, and at most 3, but 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$slice: [1, 2, 3, 4]"), json("")))
            .withMessage("[Error 28667] Expression $slice takes at least 2 arguments, and at most 3, but 4 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$slice: [1, 0, 0]"), json("")))
            .withMessage("[Error 28724] First argument to $slice must be an array, but is of type: int");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$slice: [[], 'a', 0]"), json("")))
            .withMessage("[Error 28725] Second argument to $slice must be a numeric value, but is of type: string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$slice: [[], 0, 'a']"), json("")))
            .withMessage("[Error 28725] Third argument to $slice must be numeric, but is of type: string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$slice: [[], 0, -1]"), json("")))
            .withMessage("[Error 28729] Third argument to $slice must be positive: -1");
    }

    @Test
    void testEvaluateSplit() throws Exception {
        assertThat((List<String>) Expression.evaluate(json("$split: ['June-15-2013', '-']"), json(""))).containsExactly("June", "15", "2013");
        assertThat((List<String>) Expression.evaluate(json("$split: ['$a', '$b']"), json("a: 'foo bar', b: ' '"))).containsExactly("foo", "bar");
        assertThat(Expression.evaluate(json("$split: [null, ' ']"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$split: ['$doesNotExist', ' ']"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$split: 'abc'"), json("")))
            .withMessage("[Error 16020] Expression $split takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$split: []"), json("")))
            .withMessage("[Error 16020] Expression $split takes exactly 2 arguments. 0 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$split: [1, 2, 3]"), json("")))
            .withMessage("[Error 16020] Expression $split takes exactly 2 arguments. 3 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$split: [25, ' ']"), json("")))
            .withMessage("[Error 40085] $split requires an expression that evaluates to a string as a first argument, found: int");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$split: ['foo', 10]"), json("")))
            .withMessage("[Error 40086] $split requires an expression that evaluates to a string as a second argument, found: int");
    }

    @Test
    void testEvaluateSubtract() throws Exception {
        assertThat(Expression.evaluate(json("$subtract: ['$a', '$b']"), json("a: 7, b: 5"))).isEqualTo(2);
        assertThat(Expression.evaluate(json("$subtract: [7.5, 3]"), json(""))).isEqualTo(4.5);
        assertThat(Expression.evaluate(json("$subtract: [null, 3]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$subtract: [3, null]"), json(""))).isNull();

        // subtract two instants
        assertThat(Expression.evaluate(new Document("$subtract",
            List.of(Instant.ofEpochMilli(3000), Instant.ofEpochMilli(1000))), json("")))
            .isEqualTo(2000L);

        // subtract milliseconds from instant
        assertThat(Expression.evaluate(new Document("$subtract",
            List.of(Instant.ofEpochMilli(3000), 1000)), json("")))
            .isEqualTo(Instant.ofEpochMilli(2000));

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$subtract: []"), json("")))
            .withMessage("[Error 16020] Expression $subtract takes exactly 2 arguments. 0 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$subtract: [1]"), json("")))
            .withMessage("[Error 16020] Expression $subtract takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$subtract: 123"), json("")))
            .withMessage("[Error 16020] Expression $subtract takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$subtract: ['a', 'b']"), json("")))
            .withMessage("[Error 16556] cant $subtract a string from a string");
    }

    @Test
    void testEvaluateSum() throws Exception {
        assertThat(Expression.evaluate(json("$sum: null"), json(""))).isEqualTo(0);
        assertThat(Expression.evaluate(json("$sum: ''"), json(""))).isEqualTo(0);
        assertThat(Expression.evaluate(json("$sum: 5"), json(""))).isEqualTo(5);
        assertThat(Expression.evaluate(json("$sum: [[1, 2, 3]]"), json(""))).isEqualTo(6);
        assertThat(Expression.evaluate(json("$sum: [[1], [2]]"), json(""))).isEqualTo(0);
        assertThat(Expression.evaluate(json("$sum: [1, 'foo', 2]"), json(""))).isEqualTo(3);
        assertThat(Expression.evaluate(json("$sum: ['$a', '$b']"), json("a: 7, b: 5"))).isEqualTo(12);
        assertThat(Expression.evaluate(json("$sum: []"), json(""))).isEqualTo(0);
        assertThat(Expression.evaluate(json("$sum: '$values'"), json("values: [1, 2, 3]"))).isEqualTo(6);
    }

    @Test
    void testEvaluateToLower() throws Exception {
        assertThat(Expression.evaluate(json("$toLower: null"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$toLower: '$a'"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$toLower: '$a'"), json("a: 'FOO'"))).isEqualTo("foo");
        assertThat(Expression.evaluate(json("$toLower: 1"), json(""))).isEqualTo("1");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$toLower: [[1, 2]]"), json("")))
            .withMessage("[Error 16007] can't convert from BSON type array to String");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$toLower: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $toLower takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluateToUpper() throws Exception {
        assertThat(Expression.evaluate(json("$toUpper: null"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$toUpper: '$a'"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$toUpper: '$a'"), json("a: 'foo'"))).isEqualTo("FOO");
        assertThat(Expression.evaluate(json("$toUpper: 1"), json(""))).isEqualTo("1");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$toUpper: [[1, 2]]"), json("")))
            .withMessage("[Error 16007] can't convert from BSON type array to String");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$toUpper: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $toUpper takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluateTrunc() throws Exception {
        assertThat(Expression.evaluate(json("$trunc: '$a'"), json("a: 2.5"))).isEqualTo(2);
        assertThat(Expression.evaluate(json("$trunc: 42"), json(""))).isEqualTo(42);
        assertThat(Expression.evaluate(json("$trunc: NaN"), json(""))).isEqualTo(Double.NaN);
        assertThat(Expression.evaluate(json("$trunc: [5.6]"), json(""))).isEqualTo(5);
        assertThat(Expression.evaluate(json("$trunc: ['$a']"), json("a: 9.9"))).isEqualTo(9);
        assertThat(Expression.evaluate(json("$trunc: 42.3"), json(""))).isEqualTo(42);
        assertThat(Expression.evaluate(new Document("$trunc", (double) Long.MAX_VALUE), json(""))).isEqualTo(Long.MAX_VALUE);
        assertThat(Expression.evaluate(new Document("$trunc", (double) Long.MIN_VALUE), json(""))).isEqualTo(Long.MIN_VALUE);
        assertThat(Expression.evaluate(json("$trunc: null"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$trunc: 'abc'"), json("")))
            .withMessage("[Error 28765] $trunc only supports numeric types, not string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$trunc: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $trunc takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluateToString() throws Exception {
        assertThat(Expression.evaluate(json("$toString: null"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$toString: '$a'"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$toString: '$a'"), json("a: 'foo'"))).isEqualTo("foo");
        assertThat(Expression.evaluate(json("$toString: 1"), json(""))).isEqualTo("1");
        assertThat(Expression.evaluate(json("$toString: 1.3"), json(""))).isEqualTo("1.3");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$toString: [[1, 2]]"), json("")))
            .withMessage("[Error 16007] can't convert from BSON type array to String");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$toString: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $toString takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluateSqrt() throws Exception {
        assertThat((double) Expression.evaluate(json("$sqrt: '$a'"), json("a: 2.5"))).isEqualTo(1.581, Offset.offset(0.001));
        assertThat(Expression.evaluate(json("$sqrt: 16"), json(""))).isEqualTo(4.0);
        assertThat(Expression.evaluate(json("$sqrt: [25]"), json(""))).isEqualTo(5.0);
        assertThat(Expression.evaluate(json("$sqrt: null"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$sqrt: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $sqrt takes exactly 1 arguments. 2 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$sqrt: 'abc'"), json("")))
            .withMessage("[Error 28765] $sqrt only supports numeric types, not string");
    }

    @Test
    void testEvaluateStrLenBytes() throws Exception {
        assertThat(Expression.evaluate(json("$strLenBytes: ''"), json(""))).isEqualTo(0);
        assertThat(Expression.evaluate(json("$strLenBytes: '$a'"), json("a: 'value'"))).isEqualTo(5);
        assertThat(Expression.evaluate(json("$strLenBytes: 'cafétéria'"), json(""))).isEqualTo(11);
        assertThat(Expression.evaluate(json("$strLenBytes: '$a'"), json("a: '$€λA'"))).isEqualTo(7);
        assertThat(Expression.evaluate(json("$strLenBytes: '\u5BFF\u53F8'"), json(""))).isEqualTo(6);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$strLenBytes: null"), json("")))
            .withMessage("[Error 34471] $strLenBytes requires a string argument, found: null");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$strLenBytes: 123"), json("")))
            .withMessage("[Error 34471] $strLenBytes requires a string argument, found: int");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$strLenBytes: '$a'"), json("")))
            .withMessage("[Error 34471] $strLenBytes requires a string argument, found: missing");
    }

    @Test
    void testEvaluateStrLenCP() throws Exception {
        assertThat(Expression.evaluate(json("$strLenCP: ''"), json(""))).isEqualTo(0);
        assertThat(Expression.evaluate(json("$strLenCP: '$a'"), json("a: 'value'"))).isEqualTo(5);
        assertThat(Expression.evaluate(json("$strLenCP: 'cafétéria'"), json(""))).isEqualTo(9);
        assertThat(Expression.evaluate(json("$strLenCP: '$a'"), json("a: '$€λA'"))).isEqualTo(4);
        assertThat(Expression.evaluate(json("$strLenCP: '\u5BFF\u53F8'"), json(""))).isEqualTo(2);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$strLenCP: null"), json("")))
            .withMessage("[Error 34471] $strLenCP requires a string argument, found: null");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$strLenCP: 123"), json("")))
            .withMessage("[Error 34471] $strLenCP requires a string argument, found: int");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$strLenCP: '$a'"), json("")))
            .withMessage("[Error 34471] $strLenCP requires a string argument, found: missing");
    }

    @Test
    void testEvaluateSubstrBytes() throws Exception {
        assertThat(Expression.evaluate(json("$substrBytes: ['', -1, -1]"), json(""))).isEqualTo("");
        assertThat(Expression.evaluate(json("$substrBytes: ['$a', 0, -1]"), json("a: 'value'"))).isEqualTo("value");
        assertThat(Expression.evaluate(json("$substrBytes: ['$a', 0, 5]"), json("a: 'cafétéria'"))).isEqualTo("café");
        assertThat(Expression.evaluate(json("$substrBytes: ['$a', 0, 5]"), json("a: 123"))).isEqualTo("123");
        assertThat(Expression.evaluate(json("$substrBytes: ['$a', 0, '$len']"), json("a: 'hello', len: 2"))).isEqualTo("he");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$substrBytes: null"), json("")))
            .withMessage("[Error 16020] Expression $substrBytes takes exactly 3 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$substrBytes: [123]"), json("")))
            .withMessage("[Error 16020] Expression $substrBytes takes exactly 3 arguments. 1 were passed in.");

        assertThatExceptionOfType(FailedToOptimizePipelineError.class)
            .isThrownBy(() -> Expression.evaluate(json("$substrBytes: [123, 'abc', 'def']"), json("")))
            .withMessage("[Error 16034] Failed to optimize pipeline :: caused by :: $substrBytes:  starting index must be a numeric type (is BSON type string)");

        assertThatExceptionOfType(FailedToOptimizePipelineError.class)
            .isThrownBy(() -> Expression.evaluate(json("$substrBytes: [123, 0, 'def']"), json("")))
            .withMessage("[Error 16035] Failed to optimize pipeline :: caused by :: $substrBytes:  length must be a numeric type (is BSON type string)");
    }

    @Test
    void testEvaluateSubstrCP() throws Exception {
        assertThat(Expression.evaluate(json("$substrCP: ['', -1, -1]"), json(""))).isEqualTo("");
        assertThat(Expression.evaluate(json("$substrCP: ['$a', 0, -1]"), json("a: 'value'"))).isEqualTo("value");
        assertThat(Expression.evaluate(json("$substrCP: ['$a', 0, 5]"), json("a: 'cafétéria'"))).isEqualTo("cafét");
        assertThat(Expression.evaluate(json("$substrCP: ['$a', 0, 5]"), json("a: 123"))).isEqualTo("123");
        assertThat(Expression.evaluate(json("$substrCP: ['$a', 0, '$len']"), json("a: 'hello', len: 2"))).isEqualTo("he");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$substrCP: null"), json("")))
            .withMessage("[Error 16020] Expression $substrCP takes exactly 3 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$substrCP: [123]"), json("")))
            .withMessage("[Error 16020] Expression $substrCP takes exactly 3 arguments. 1 were passed in.");

        assertThatExceptionOfType(FailedToOptimizePipelineError.class)
            .isThrownBy(() -> Expression.evaluate(json("$substrCP: [123, 'abc', 'def']"), json("")))
            .withMessage("[Error 34450] Failed to optimize pipeline :: caused by :: $substrCP: starting index must be a numeric type (is BSON type string)");

        assertThatExceptionOfType(FailedToOptimizePipelineError.class)
            .isThrownBy(() -> Expression.evaluate(json("$substrCP: [123, 0, 'def']"), json("")))
            .withMessage("[Error 34452] Failed to optimize pipeline :: caused by :: $substrCP: length must be a numeric type (is BSON type string)");
    }

    @Test
    void testEvaluateYear() throws Exception {
        assertThat(Expression.evaluate(json("$year: '$a'"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$year: '$a'"), new Document("a", toDate("2018-07-03T14:00:00Z")))).isEqualTo(2018);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$year: '$a'"), json("a: 'abc'")))
            .withMessage("[Error 16006] can't convert from string to Date");
    }

    @Test
    void testEvaluateDayOfWeek() throws Exception {
        assertThat(Expression.evaluate(json("$dayOfWeek: '$a'"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$dayOfWeek: '$a'"), new Document("a", toDate("2018-01-01T14:00:00Z")))).isEqualTo(1);
        assertThat(Expression.evaluate(json("$dayOfWeek: '$a'"), new Document("a", toDate("2014-02-03T14:00:00Z")))).isEqualTo(1);
        assertThat(Expression.evaluate(json("$dayOfWeek: '$a'"), new Document("a", toDate("2018-11-08T22:00:00Z")))).isEqualTo(4);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dayOfWeek: '$a'"), json("a: 'abc'")))
            .withMessage("[Error 16006] can't convert from string to Date");
    }

    @Test
    void testEvaluateDayOfMonth() throws Exception {
        assertThat(Expression.evaluate(json("$dayOfMonth: '$a'"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$dayOfMonth: '$a'"), new Document("a", toDate("2018-01-01T14:00:00Z")))).isEqualTo(1);
        assertThat(Expression.evaluate(json("$dayOfMonth: '$a'"), new Document("a", toDate("2014-02-03T14:00:00Z")))).isEqualTo(3);
        assertThat(Expression.evaluate(json("$dayOfMonth: '$a'"), new Document("a", toDate("2018-11-08T22:00:00Z")))).isEqualTo(8);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dayOfMonth: '$a'"), json("a: 'abc'")))
            .withMessage("[Error 16006] can't convert from string to Date");
    }

    @Test
    void testEvaluateDayOfYear() throws Exception {
        assertThat(Expression.evaluate(json("$dayOfYear: '$a'"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$dayOfYear: '$a'"), new Document("a", toDate("2018-01-01T14:00:00Z")))).isEqualTo(1);
        assertThat(Expression.evaluate(json("$dayOfYear: '$a'"), new Document("a", toDate("2014-02-03T14:00:00Z")))).isEqualTo(34);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dayOfYear: '$a'"), json("a: 'abc'")))
            .withMessage("[Error 16006] can't convert from string to Date");
    }

    @Test
    void testDateToString() throws Exception {
        Instant instant = Instant.parse("2011-12-19T10:15:20.250Z");

        assertThat(Expression.evaluate(json("$dateToString: {date: '$a'}"), json(""))).isNull();
        // default format
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a'}"), new Document("a", instant))).isEqualTo("2011-12-19T10:15:20.250Z");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', timezone: '+01:00'}"), new Document("a", instant))).isEqualTo("2011-12-19T11:15:20.250Z");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', onNull: '1970-01-01T00:00:00Z'}"), new Document("a", null))).isEqualTo("1970-01-01T00:00:00Z");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: 'foo'}"), new Document("a", instant))).isEqualTo("foo");

        // test different formats (https://docs.mongodb.com/manual/reference/operator/aggregation/dateToString/#format-specifiers)
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%d'}"), new Document("a", instant))).isEqualTo("19");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%G'}"), new Document("a", instant))).isEqualTo("2011");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%H'}"), new Document("a", instant))).isEqualTo("10");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%j'}"), new Document("a", instant))).isEqualTo("353");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%L'}"), new Document("a", instant))).isEqualTo("250");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%m'}"), new Document("a", instant))).isEqualTo("12");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%M'}"), new Document("a", instant))).isEqualTo("15");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%S'}"), new Document("a", instant))).isEqualTo("20");
        // assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%w'}"), new Document("a", instant))).isEqualTo("2");
        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dateToString: {date: '$a', format: '%w'}"), new Document("a", instant)))
            .withMessage("[Error 18536] Not yet supported format character '%w' in $dateToString format string");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%u'}"), new Document("a", instant))).isEqualTo("1");
        // assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%U'}"), new Document("a", instant))).isEqualTo("51");
        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dateToString: {date: '$a', format: '%U'}"), new Document("a", instant)))
            .withMessage("[Error 18536] Not yet supported format character '%U' in $dateToString format string");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%V'}"), new Document("a", instant))).isEqualTo("51");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%Y'}"), new Document("a", instant))).isEqualTo("2011");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%z'}"), new Document("a", instant))).isEqualTo("+0000");
        // assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%Z'}"), new Document("a", instant))).isEqualTo("+000");
        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dateToString: {date: '$a', format: '%Z'}"), new Document("a", instant)))
            .withMessage("[Error 18536] Not yet supported format character '%Z' in $dateToString format string");
        assertThat(Expression.evaluate(json("$dateToString: {date: '$a', format: '%%'}"), new Document("a", instant))).isEqualTo("%");
        // empty format specifier
        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dateToString: {date: '$a', format: '%'}"), new Document("a", instant)))
            .withMessage("[Error 18535] Unmatched '%' at end of $dateToString format string");
        // invalid format specifier
        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dateToString: {date: '$a', format: '% '}"), new Document("a", instant)))
            .withMessage("[Error 18536] Invalid format character '% ' in $dateToString format string");

        // validation errors
        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dateToString: ''"), json("")))
            .withMessage("[Error 18629] $dateToString only supports an object as its argument");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dateToString: {}"), json("")))
            .withMessage("[Error 18628] Missing 'date' parameter to $dateToString");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dateToString: {date: null, foo: 1}"), json("")))
            .withMessage("[Error 18534] Unrecognized parameter to $dateToString: foo");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dateToString: {date: null, format: 1}"), json("")))
            .withMessage("[Error 18533] $dateToString requires that 'format' be a string, found: int with value 1");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dateToString: {date: null, format: '', timezone: 'foo'}"), json("")))
            .withMessage("[Error 40485] $dateToString unrecognized time zone identifier: foo");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$dateToString: {date: 'foo'}"), json("")))
            .withMessage("[Error 16006] can't convert from string to Date");
    }

    @Test
    void testEvaluateDivide() throws Exception {
        assertThat(Expression.evaluate(json("$divide: ['$a', '$b']"), json("a: 8, b: 4"))).isEqualTo(2.0);
        assertThat(Expression.evaluate(json("$divide: [4.5, 3]"), json(""))).isEqualTo(1.5);
        assertThat(Expression.evaluate(json("$divide: [null, 3]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$divide: [null, null]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$divide: [3, null]"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$divide: []"), json("")))
            .withMessage("[Error 16020] Expression $divide takes exactly 2 arguments. 0 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$divide: [1, 0]"), json("")))
            .withMessage("[Error 16608] can't $divide by zero");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$divide: [1]"), json("")))
            .withMessage("[Error 16020] Expression $divide takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$divide: 123"), json("")))
            .withMessage("[Error 16020] Expression $divide takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$divide: ['a', 'b']"), json("")))
            .withMessage("[Error 16609] $divide only supports numeric types, not string and string");
    }

    @Test
    void testEvaluateExp() throws Exception {
        assertThat(Expression.evaluate(json("$exp: 0"), json(""))).isEqualTo(1.0);
        assertThat(Expression.evaluate(json("$exp: [0]"), json(""))).isEqualTo(1.0);
        assertThat((double) Expression.evaluate(json("$exp: '$a'"), json("a: 2"))).isEqualTo(7.389, Offset.offset(0.001));
        assertThat((double) Expression.evaluate(json("$exp: '$a.b'"), json("a: {b: -2}"))).isEqualTo(0.135, Offset.offset(0.001));
        assertThat(Expression.evaluate(json("$exp: '$doesNotExist'"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$exp: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $exp takes exactly 1 arguments. 2 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$exp: ['a']"), json("")))
            .withMessage("[Error 28765] $exp only supports numeric types, not string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$exp: 'a'"), json("")))
            .withMessage("[Error 28765] $exp only supports numeric types, not string");
    }

    @Test
    void testEvaluateFilter() throws Exception {
        assertThat(Expression.evaluate(json("$filter: {input: null, cond: null}"), json(""))).isNull();

        assertThat(Expression.evaluate(json("$filter: {input: [1, 2, 3, 4], as: 'value', cond: {$lte: ['$$value', 3]}}"), json("")))
            .isEqualTo(List.of(1, 2, 3));

        assertThat(Expression.evaluate(json("$filter: {input: [1, 2, 3, 4], cond: {$lte: ['$$this', 3]}}"), json("")))
            .isEqualTo(List.of(1, 2, 3));

        assertThat(Expression.evaluate(json("$filter: {input: [1, 2, 3, 4], cond: {$lt: ['$$this', '$$ROOT.thresh']}}"), json("thresh: 3")))
            .isEqualTo(List.of(1, 2));

        assertThat(Expression.evaluate(json("$filter: {input: [1, 2, 3], cond: 1}"), json("")))
            .isEqualTo(List.of(1, 2, 3));

        assertThat(Expression.evaluate(json("$filter: {input: '$doesNotExist', cond: 1}"), json("")))
            .isNull();

        assertThat(Expression.evaluate(json("$filter: {input: '$items', as: 'item', cond: {$gte: ['$$item.price', 10]}}"),
            json("items: [{item_id: 1, price: 110}, {item_id: 2, price: 5}, {item_id: 3, price: 50}]")))
            .isEqualTo(List.of(
                json("item_id: 1, price: 110"),
                json("item_id: 3, price: 50")
            ));

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$filter: 'a'"), json("")))
            .withMessage("[Error 28646] $filter only supports an object as its argument");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Expression.evaluate(json("$filter: {input: [1, 2, 3], cond: true}"), json("$this: 1")))
            .withMessage("Document already contains '$this'");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$filter: {input: 'a', cond: null}"), json("")))
            .withMessage("[Error 28651] input to $filter must be an array not string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$filter: {}"), json("")))
            .withMessage("[Error 28648] Missing 'input' parameter to $filter");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$filter: {input: null}"), json("")))
            .withMessage("[Error 28648] Missing 'cond' parameter to $filter");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$filter: {input: 1, cond: 1, foo: 1}"), json("")))
            .withMessage("[Error 28647] Unrecognized parameter to $filter: foo");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$filter: {input: [], as: [], cond: 1}"), json("")))
            .withMessage("[Error 16866] empty variable names are not allowed");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$filter: {input: [], as: '', cond: 1}"), json("")))
            .withMessage("[Error 16866] empty variable names are not allowed");
    }

    @Test
    void testEvaluateFloor() throws Exception {
        assertThat(Expression.evaluate(json("$floor: '$a'"), json("a: 2.5"))).isEqualTo(2);
        assertThat(Expression.evaluate(json("$floor: 42"), json(""))).isEqualTo(42);
        assertThat(Expression.evaluate(json("$floor: NaN"), json(""))).isEqualTo(Double.NaN);
        assertThat(Expression.evaluate(json("$floor: [5.6]"), json(""))).isEqualTo(5);
        assertThat(Expression.evaluate(json("$floor: ['$a']"), json("a: 9.9"))).isEqualTo(9);
        assertThat(Expression.evaluate(json("$floor: 42.3"), json(""))).isEqualTo(42);
        assertThat(Expression.evaluate(new Document("$floor", (double) Long.MAX_VALUE), json(""))).isEqualTo(Long.MAX_VALUE);
        assertThat(Expression.evaluate(new Document("$floor", (double) Long.MIN_VALUE), json(""))).isEqualTo(Long.MIN_VALUE);
        assertThat(Expression.evaluate(json("$floor: null"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$floor: 'abc'"), json("")))
            .withMessage("[Error 28765] $floor only supports numeric types, not string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$floor: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $floor takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluateIfNull() throws Exception {
        assertThat(Expression.evaluate(json("$ifNull: [17, 'Unspecified']"), json(""))).isEqualTo(17);
        assertThat(Expression.evaluate(json("$ifNull: [null, null]"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$ifNull: ['$desc', 'Unspecified']"), json(""))).isEqualTo("Unspecified");
        assertThat(Expression.evaluate(json("$ifNull: ['$desc', 'Unspecified']"), json("desc: null"))).isEqualTo("Unspecified");
        assertThat(Expression.evaluate(json("$ifNull: ['$desc', 'Unspecified']"), json("desc: 'prod1'"))).isEqualTo("prod1");
        assertThat(Expression.evaluate(json("$ifNull: ['$desc', '$alt']"), json("alt: 'prod'"))).isEqualTo("prod");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$ifNull: []"), json("")))
            .withMessage("[Error 16020] Expression $ifNull takes exactly 2 arguments. 0 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$ifNull: ['abc']"), json("")))
            .withMessage("[Error 16020] Expression $ifNull takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$ifNull: 'abc'"), json("")))
            .withMessage("[Error 16020] Expression $ifNull takes exactly 2 arguments. 1 were passed in.");
    }

    @Test
    void testEvaluateIn() throws Exception {
        assertThat(Expression.evaluate(json("$in: [2, [1, 2, 3]]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$in: ['abc', ['xyz', 'abc']]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$in: [['a'], ['a']]"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$in: [['a'], [['a']]]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$in: ['bananas', '$fruits']"), json("fruits: ['apples', 'oranges']"))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$in: ['bananas', '$fruits']"), json("fruits: ['apples', 'bananas', 'oranges']"))).isEqualTo(true);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$in: ['abc']"), json("")))
            .withMessage("[Error 16020] Expression $in takes exactly 2 arguments. 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$in: ['a', 'b']"), json("")))
            .withMessage("[Error 40081] $in requires an array as a second argument, found: string");
    }

    @Test
    void testEvaluateIndexOfArray() throws Exception {
        assertThat(Expression.evaluate(json("$indexOfArray: [['a', 'abc'], 'a']"), json(""))).isEqualTo(0);
        assertThat(Expression.evaluate(json("$indexOfArray: [['a', 'abc', 'de', ['de']], ['de']]"), json(""))).isEqualTo(3);
        assertThat(Expression.evaluate(json("$indexOfArray: [[1, 2], 5]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfArray: [[1, 2, 3], [1, 2]]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfArray: [[10, 9, 9, 8, 9], 9, 3]"), json(""))).isEqualTo(4);
        assertThat(Expression.evaluate(json("$indexOfArray: [['a', 'abc', 'b'], 'b', 0, 1]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfArray: [['a', 'abc', 'b'], 'b', 1, 0]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfArray: [['a', 'abc', 'b'], 'b', 20]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfArray: [[null, null, null], null]"), json(""))).isEqualTo(0);
        assertThat(Expression.evaluate(json("$indexOfArray: [null, 'foo']"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$indexOfArray: ['$items', 2]"), json("items: [3, 4, 5, 2]"))).isEqualTo(3);
        assertThat(Expression.evaluate(json("$indexOfArray: ['$items', 2]"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfArray: ['a']"), json("")))
            .withMessage("[Error 28667] Expression $indexOfArray takes at least 2 arguments, and at most 4, but 1 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfArray: [['a'], 'a', 'illegalIndex']"), json("")))
            .withMessage("[Error 40096] $indexOfArray requires an integral starting index, found a value of type: string, with value: \"illegalIndex\"");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfArray: [['a'], 'a', 0, 'illegalIndex']"), json("")))
            .withMessage("[Error 40096] $indexOfArray requires an integral ending index, found a value of type: string, with value: \"illegalIndex\"");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfArray: [['a'], 'a', -1]"), json("")))
            .withMessage("[Error 40097] $indexOfArray requires a nonnegative starting index, found: -1");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfArray: [['a'], 'a', 0, -1]"), json("")))
            .withMessage("[Error 40097] $indexOfArray requires a nonnegative ending index, found: -1");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfArray: [1, 2, 3, 4, 5]"), json("")))
            .withMessage("[Error 28667] Expression $indexOfArray takes at least 2 arguments, and at most 4, but 5 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfArray: ['a', 'b']"), json("")))
            .withMessage("[Error 40090] $indexOfArray requires an array as a first argument, found: string");
    }

    @Test
    void testEvaluateIndexOfBytes() throws Exception {
        assertThat(Expression.evaluate(json("$indexOfBytes: ['cafeteria', 'e']"), json(""))).isEqualTo(3);
        assertThat(Expression.evaluate(json("$indexOfBytes: ['cafétéria', 'é']"), json(""))).isEqualTo(3);
        assertThat(Expression.evaluate(json("$indexOfBytes: ['cafétéria', 'e']"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfBytes: ['cafétéria', 't']"), json(""))).isEqualTo(5);
        assertThat(Expression.evaluate(json("$indexOfBytes: ['foo.bar.fi', '.', 5]"), json(""))).isEqualTo(7);
        assertThat(Expression.evaluate(json("$indexOfBytes: ['vanilla', 'll', 0, 2]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfBytes: ['vanilla', 'll', 12]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfBytes: ['vanilla', 'll', 5, 2]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfBytes: ['vanilla', 'nilla', 3]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfBytes: [null, 'foo']"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$indexOfBytes: ['$text', 'world']"), json("text: 'hello world'"))).isEqualTo(6);
        assertThat(Expression.evaluate(json("$indexOfBytes: ['$text', '$search']"), json("text: 'hello world', search: 'l'"))).isEqualTo(2);
        assertThat(Expression.evaluate(json("$indexOfBytes: ['$text', '$search']"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfBytes: []"), json("")))
            .withMessage("[Error 28667] Expression $indexOfBytes takes at least 2 arguments, and at most 4, but 0 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfBytes: [1, 2, 3, 4, 5]"), json("")))
            .withMessage("[Error 28667] Expression $indexOfBytes takes at least 2 arguments, and at most 4, but 5 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfBytes: [[], 'll']"), json("")))
            .withMessage("[Error 40091] $indexOfBytes requires a string as the first argument, found: array");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfBytes: ['foo', ['x']]"), json("")))
            .withMessage("[Error 40092] $indexOfBytes requires a string as the second argument, found: array");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfBytes: ['vanilla', 'll', -1]"), json("")))
            .withMessage("[Error 40097] $indexOfBytes requires a nonnegative starting index, found: -1");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfBytes: ['vanilla', 'll', 0, -1]"), json("")))
            .withMessage("[Error 40097] $indexOfBytes requires a nonnegative ending index, found: -1");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfBytes: ['vanilla', 'll', 'a']"), json("")))
            .withMessage("[Error 40096] $indexOfBytes requires an integral starting index, found a value of type: string, with value: \"a\"");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfBytes: ['vanilla', 'll', 0, 'b']"), json("")))
            .withMessage("[Error 40096] $indexOfBytes requires an integral ending index, found a value of type: string, with value: \"b\"");
    }

    @Test
    void testEvaluateIndexOfCP() throws Exception {
        assertThat(Expression.evaluate(json("$indexOfCP: ['cafeteria', 'e']"), json(""))).isEqualTo(3);
        assertThat(Expression.evaluate(json("$indexOfCP: ['cafétéria', 'é']"), json(""))).isEqualTo(3);
        assertThat(Expression.evaluate(json("$indexOfCP: ['cafétéria', 'e']"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfCP: ['cafétéria', 't']"), json(""))).isEqualTo(4);
        assertThat(Expression.evaluate(json("$indexOfCP: ['foo.bar.fi', '.', 5]"), json(""))).isEqualTo(7);
        assertThat(Expression.evaluate(json("$indexOfCP: ['vanilla', 'll', 0, 2]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfCP: ['vanilla', 'll', 12]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfCP: ['vanilla', 'll', 5, 2]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfCP: ['vanilla', 'nilla', 3]"), json(""))).isEqualTo(-1);
        assertThat(Expression.evaluate(json("$indexOfCP: [null, 'foo']"), json(""))).isNull();
        assertThat(Expression.evaluate(json("$indexOfCP: ['$text', 'world']"), json("text: 'hello world'"))).isEqualTo(6);
        assertThat(Expression.evaluate(json("$indexOfCP: ['$text', '$search']"), json("text: 'hello world', search: 'l'"))).isEqualTo(2);
        assertThat(Expression.evaluate(json("$indexOfCP: ['$text', '$search']"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfCP: []"), json("")))
            .withMessage("[Error 28667] Expression $indexOfCP takes at least 2 arguments, and at most 4, but 0 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfCP: [1, 2, 3, 4, 5]"), json("")))
            .withMessage("[Error 28667] Expression $indexOfCP takes at least 2 arguments, and at most 4, but 5 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfCP: [[], 'll']"), json("")))
            .withMessage("[Error 40093] $indexOfCP requires a string as the first argument, found: array");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfCP: ['foo', ['x']]"), json("")))
            .withMessage("[Error 40094] $indexOfCP requires a string as the second argument, found: array");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfCP: ['vanilla', 'll', -1]"), json("")))
            .withMessage("[Error 40097] $indexOfCP requires a nonnegative starting index, found: -1");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfCP: ['vanilla', 'll', 0, -1]"), json("")))
            .withMessage("[Error 40097] $indexOfCP requires a nonnegative ending index, found: -1");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfCP: ['vanilla', 'll', 'a']"), json("")))
            .withMessage("[Error 40096] $indexOfCP requires an integral starting index, found a value of type: string, with value: \"a\"");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$indexOfCP: ['vanilla', 'll', 0, 'b']"), json("")))
            .withMessage("[Error 40096] $indexOfCP requires an integral ending index, found a value of type: string, with value: \"b\"");
    }

    @Test
    void testEvaluateIsArray() throws Exception {
        assertThat(Expression.evaluate(json("$isArray: ['hello']"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$isArray: [[2, 3]]"), json(""))).isEqualTo(true);
        assertThat(Expression.evaluate(json("$isArray: 'foo'}"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$isArray: null}"), json(""))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$isArray: '$value'}"), json("value: 'abc'"))).isEqualTo(false);
        assertThat(Expression.evaluate(json("$isArray: '$value'}"), json("value: ['abc']"))).isEqualTo(true);

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$isArray: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $isArray takes exactly 1 arguments. 2 were passed in.");
    }

    @Test
    void testEvaluateLiteral() throws Exception {
        assertThat(Expression.evaluate(json("$literal: {$add: [2, 3]}"), json(""))).isEqualTo(json("$add: [2, 3]"));
        assertThat(Expression.evaluate(json("$literal: {$literal: 1}"), json(""))).isEqualTo(json("$literal: 1"));
    }

    @Test
    void testEvaluateLn() throws Exception {
        assertThat(Expression.evaluate(json("$ln: 1"), json(""))).isEqualTo(0.0);
        assertThat(Expression.evaluate(json("$ln: [1]"), json(""))).isEqualTo(0.0);
        assertThat((double) Expression.evaluate(json("$ln: '$a'"), json("a: 10"))).isEqualTo(2.302, Offset.offset(0.001));
        assertThat(Expression.evaluate(json("$ln: '$a.b'"), json("a: {b: -2}"))).isEqualTo(Double.NaN);
        assertThat(Expression.evaluate(json("$ln: '$doesNotExist'"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$ln: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $ln takes exactly 1 arguments. 2 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$ln: ['a']"), json("")))
            .withMessage("[Error 28765] $ln only supports numeric types, not string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$ln: 'a'"), json("")))
            .withMessage("[Error 28765] $ln only supports numeric types, not string");
    }

    @Test
    void testEvaluateLog() throws Exception {
        assertThat(Expression.evaluate(json("$log: 1"), json(""))).isEqualTo(0.0);
        assertThat(Expression.evaluate(json("$log: [1]"), json(""))).isEqualTo(0.0);
        assertThat((double) Expression.evaluate(json("$log: '$a'"), json("a: 10"))).isEqualTo(2.302, Offset.offset(0.001));
        assertThat(Expression.evaluate(json("$log: '$a.b'"), json("a: {b: -2}"))).isEqualTo(Double.NaN);
        assertThat(Expression.evaluate(json("$log: '$doesNotExist'"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$log: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $log takes exactly 1 arguments. 2 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$log: ['a']"), json("")))
            .withMessage("[Error 28765] $log only supports numeric types, not string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$log: 'a'"), json("")))
            .withMessage("[Error 28765] $log only supports numeric types, not string");
    }

    @Test
    void testEvaluateLog10() throws Exception {
        assertThat(Expression.evaluate(json("$log10: 1"), json(""))).isEqualTo(0.0);
        assertThat(Expression.evaluate(json("$log10: 10"), json(""))).isEqualTo(1.0);
        assertThat(Expression.evaluate(json("$log10: 100"), json(""))).isEqualTo(2.0);
        assertThat(Expression.evaluate(json("$log10: 1000"), json(""))).isEqualTo(3.0);
        assertThat(Expression.evaluate(json("$log10: [1]"), json(""))).isEqualTo(0.0);
        assertThat((double) Expression.evaluate(json("$log10: '$a'"), json("a: 20"))).isEqualTo(1.301, Offset.offset(0.001));
        assertThat(Expression.evaluate(json("$log10: '$a.b'"), json("a: {b: -2}"))).isEqualTo(Double.NaN);
        assertThat(Expression.evaluate(json("$log10: '$doesNotExist'"), json(""))).isNull();

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$log10: [1, 2]"), json("")))
            .withMessage("[Error 16020] Expression $log10 takes exactly 1 arguments. 2 were passed in.");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$log10: ['a']"), json("")))
            .withMessage("[Error 28765] $log10 only supports numeric types, not string");

        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$log10: 'a'"), json("")))
            .withMessage("[Error 28765] $log10 only supports numeric types, not string");
    }

    @Test
    void testEvaluateIllegalExpression() throws Exception {
        assertThatExceptionOfType(MongoServerError.class)
            .isThrownBy(() -> Expression.evaluate(json("$foo: '$a'"), json("")))
            .withMessage("[Error 168] Unrecognized expression '$foo'");
    }

    @Test
    void testEvaluateDocument_SimpleExpression() throws Exception {
        Object evaluatedDocument = Expression.evaluateDocument(json("key: '$key2'"), json("key2: 123"));
        assertThat(evaluatedDocument).isEqualTo(json("key: 123"));
    }

    @Test
    void testEvaluateDocument_NullValue() throws Exception {
        Object evaluatedDocument = Expression.evaluateDocument(json("key: '$value'"), json("value: null"));
        assertThat(evaluatedDocument).isEqualTo(json("key: null"));
    }

    // https://github.com/bwaldvogel/mongo-java-server/issues/111
    @Test
    void testEvaluateDocument_MissingValue() throws Exception {
        Object evaluatedDocument = Expression.evaluateDocument(json("key: '$missing'"), json(""));
        assertThat(evaluatedDocument).isEqualTo(json(""));
    }

    private static Instant toDate(String instant) {
        return Instant.parse(instant);
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/ReadOnlyProxy.java
package de.bwaldvogel.mongo.backend;

import java.time.Clock;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import de.bwaldvogel.mongo.MongoBackend;
import de.bwaldvogel.mongo.MongoDatabase;
import de.bwaldvogel.mongo.ServerVersion;
import de.bwaldvogel.mongo.backend.aggregation.Aggregation;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.MongoServerException;
import de.bwaldvogel.mongo.exception.NoSuchCommandException;
import de.bwaldvogel.mongo.wire.message.MongoMessage;
import de.bwaldvogel.mongo.wire.message.MongoQuery;
import io.netty.channel.Channel;

public class ReadOnlyProxy implements MongoBackend {

    private static final Set<String> allowedCommands = Set.of(
        "ismaster",
        "find",
        "listdatabases",
        "count",
        "dbstats",
        "distinct",
        "collstats",
        "serverstatus",
        "buildinfo",
        "getlasterror",
        "getmore"
    );

    private final MongoBackend backend;

    public ReadOnlyProxy(MongoBackend backend) {
        this.backend = backend;
    }

    public static class ReadOnlyException extends MongoServerException {

        private static final long serialVersionUID = 1L;

        ReadOnlyException(String message) {
            super(message);
        }

    }

    @Override
    public void handleClose(Channel channel) {
        backend.handleClose(channel);
    }

    @Override
    public Document handleCommand(Channel channel, String database, String command, Document query) {
        if (isAllowed(command, query)) {
            return backend.handleCommand(channel, database, command, query);
        }
        throw new NoSuchCommandException(command);
    }

    private static boolean isAllowed(String command, Document query) {
        if (allowedCommands.contains(command.toLowerCase())) {
            return true;
        }

        if (command.equalsIgnoreCase("aggregate")) {
            List<Document> pipeline = Aggregation.parse(query.get("pipeline"));
            Aggregation aggregation = Aggregation.fromPipeline(pipeline, null, null, null, null);
            if (aggregation.isModifying()) {
                throw new MongoServerException("Aggregation contains a modifying stage and is therefore not allowed in read-only mode");
            }
            return true;
        }

        return false;
    }

    @Override
    public Document handleMessage(MongoMessage message) {
        Document document = message.getDocument();
        String command = document.keySet().iterator().next().toLowerCase();
        if (isAllowed(command, document)) {
            return backend.handleMessage(message);
        }
        throw new NoSuchCommandException(command);
    }

    @Override
    public Collection<Document> getCurrentOperations(MongoQuery query) {
        return backend.getCurrentOperations(query);
    }

    @Override
    public QueryResult handleQuery(MongoQuery query) {
        return backend.handleQuery(query);
    }

    @Override
    public void dropDatabase(String database) {
        throw new ReadOnlyException("dropping of databases is not allowed");
    }

    @Override
    public MongoBackend version(ServerVersion version) {
        throw new ReadOnlyException("not supported");
    }

    @Override
    public MongoDatabase resolveDatabase(String database) {
        throw new ReadOnlyException("resolveDatabase not allowed");
    }

    @Override
    public Document getServerStatus() {
        return backend.getServerStatus();
    }

    @Override
    public void close() {
        backend.close();
    }

    @Override
    public Clock getClock() {
        return backend.getClock();
    }

    @Override
    public void enableOplog() {
    }

    @Override
    public void disableOplog() {
    }

    @Override
    public void closeCursors(List<Long> cursorIds) {
        backend.closeCursors(cursorIds);
    }
}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/wire/MongoDatabaseHandlerTest.java
package de.bwaldvogel.mongo.wire;

import static de.bwaldvogel.mongo.TestUtils.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.Test;

import de.bwaldvogel.mongo.MongoBackend;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.wire.message.MessageHeader;
import de.bwaldvogel.mongo.wire.message.MongoMessage;
import de.bwaldvogel.mongo.wire.message.MongoQuery;
import io.netty.channel.Channel;

class MongoDatabaseHandlerTest {

    @Test
    void testWrappedCommand() throws Exception {
        MongoBackend backend = mock(MongoBackend.class);
        Channel channel = mock(Channel.class);

        Document queryDoc = json("'$query': { 'count': 'collectionName' }, '$readPreference': { 'mode': 'secondaryPreferred' }");
        Document subQueryDoc = json("'count': 'collectionName'");
        MongoQuery query = new MongoQuery(channel, null, "dbName.$cmd", 0, 0, queryDoc, null);

        MongoDatabaseHandler handler = new MongoDatabaseHandler(backend, null);

        handler.handleCommand(query);

        verify(backend).handleCommand(channel, "dbName", "count", subQueryDoc);
    }

    @Test
    void testNonWrappedCommand() throws Exception {
        MongoBackend backend = mock(MongoBackend.class);
        Channel channel = mock(Channel.class);

        Document queryDoc = json("'count': 'collectionName'");
        MongoQuery query = new MongoQuery(channel, null, "dbName.$cmd", 0, 0, queryDoc, null);

        MongoDatabaseHandler handler = new MongoDatabaseHandler(backend, null);

        handler.handleCommand(query);

        verify(backend).handleCommand(channel, "dbName", "count", queryDoc);
    }

    @Test
    void testHandleMessageUnknownError() throws Exception {
        MongoBackend backend = mock(MongoBackend.class);
        when(backend.handleMessage(any())).thenThrow(new RuntimeException("unexpected"));

        Channel channel = mock(Channel.class);

        MessageHeader header = new MessageHeader(0, 0);

        MongoDatabaseHandler handler = new MongoDatabaseHandler(backend, null);

        MongoMessage requestMessage = new MongoMessage(channel, header, new Document("key", "1"));

        MongoMessage responseMessage = handler.handleMessage(requestMessage);

        assertThat(responseMessage).isNotNull();
        Document responseMessageDoc = responseMessage.getDocument();
        assertThat(responseMessageDoc).isNotNull();
        assertThat(responseMessageDoc.get("$err")).isEqualTo("Unknown error: unexpected");
        assertThat(responseMessageDoc.get("errmsg")).isEqualTo("Unknown error: unexpected");
        assertThat(responseMessageDoc.get("ok")).isEqualTo(0);
    }
}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/NumericUtilsTest.java
package de.bwaldvogel.mongo.backend;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.math.BigDecimal;

import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Test;

import de.bwaldvogel.mongo.bson.Decimal128;

public class NumericUtilsTest {

    @Test
    void testAddNumbers() {
        assertThat(NumericUtils.addNumbers(-4, 3)).isEqualTo(-1);
        assertThat(NumericUtils.addNumbers(-0.1, 0.1)).isEqualTo(0.0);
        assertThat(NumericUtils.addNumbers(0.9, 0.1)).isEqualTo(1.0);
        assertThat(NumericUtils.addNumbers(4.3f, 7.1f)).isEqualTo(11.4f);
        assertThat(NumericUtils.addNumbers((short) 4, (short) 7)).isEqualTo((short) 11);
        assertThat(NumericUtils.addNumbers((short) 30000, (short) 20000)).isEqualTo(50000L);
        assertThat(NumericUtils.addNumbers(4L, 7.3)).isEqualTo(11.3);
        assertThat(NumericUtils.addNumbers(100000000000000L, 100000000000000L)).isEqualTo(200000000000000L);
        assertThat(NumericUtils.addNumbers(2000000000, 2000000000)).isEqualTo(4000000000L);
        assertThat(NumericUtils.addNumbers(Decimal128.fromNumber(1L), Decimal128.fromNumber(2.5))).isEqualTo(Decimal128.fromNumber(3.5));

        assertThatExceptionOfType(UnsupportedOperationException.class)
            .isThrownBy(() -> NumericUtils.addNumbers(new BigDecimal(1), new BigDecimal(1)))
            .withMessage("cannot calculate on 1 and 1");
    }

    @Test
    void testSubtractNumbers() {
        assertThat(NumericUtils.subtractNumbers(-4, 3)).isEqualTo(-7);
        assertThat(NumericUtils.subtractNumbers(0.1, 0.1)).isEqualTo(0.0);
        assertThat(NumericUtils.subtractNumbers(1.1, 0.1)).isEqualTo(1.0);
        assertThat(NumericUtils.subtractNumbers(7.6f, 4.1f)).isEqualTo(3.5f);
        assertThat(NumericUtils.subtractNumbers((short) 4, (short) 7)).isEqualTo((short) -3);
        assertThat(NumericUtils.subtractNumbers((short) 30000, (short) -20000)).isEqualTo(50000L);
        assertThat(NumericUtils.subtractNumbers(4L, 7.3)).isEqualTo(-3.3);
        assertThat(NumericUtils.subtractNumbers(100000000000000L, 1L)).isEqualTo(99999999999999L);
        assertThat(NumericUtils.subtractNumbers(-2000000000, 2000000000)).isEqualTo(-4000000000L);
        assertThat(NumericUtils.subtractNumbers(Decimal128.fromNumber(1005L), Decimal128.fromNumber(2.5))).isEqualTo(Decimal128.fromNumber(1002.5));

        assertThatExceptionOfType(UnsupportedOperationException.class)
            .isThrownBy(() -> NumericUtils.subtractNumbers(new BigDecimal(1), new BigDecimal(1)))
            .withMessage("cannot calculate on 1 and 1");
    }

    @Test
    void testMultiplyNumbers() {
        assertThat(NumericUtils.multiplyNumbers(-4, 3)).isEqualTo(-12);
        assertThat((double) NumericUtils.multiplyNumbers(0.1, 0.1)).isEqualTo(0.01, Offset.offset(0.0001));
        assertThat((double) NumericUtils.multiplyNumbers(1.1, 0.1)).isEqualTo(0.11, Offset.offset(0.0001));
        assertThat(NumericUtils.multiplyNumbers(2.0f, 4.0f)).isEqualTo(8.0f);
        assertThat(NumericUtils.multiplyNumbers((short) 4, (short) 7)).isEqualTo((short) 28);
        assertThat(NumericUtils.multiplyNumbers((short) 400, (short) 700)).isEqualTo(280000L);
        assertThat(NumericUtils.multiplyNumbers(100000000000000L, 10)).isEqualTo(1000000000000000L);
        assertThat(NumericUtils.multiplyNumbers(50000, 100000)).isEqualTo(5000000000L);
        assertThat(NumericUtils.multiplyNumbers(Decimal128.fromNumber(1000), Decimal128.fromNumber(2000.5))).isEqualTo(Decimal128.fromNumber(2000500.0));

        assertThatExceptionOfType(UnsupportedOperationException.class)
            .isThrownBy(() -> NumericUtils.multiplyNumbers(new BigDecimal(1), new BigDecimal(1)))
            .withMessage("cannot calculate on 1 and 1");
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/wire/MongoWireMessageEncoder.java
package de.bwaldvogel.mongo.wire;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.wire.bson.BsonEncoder;
import de.bwaldvogel.mongo.wire.message.MongoMessage;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class MongoWireMessageEncoder extends MessageToByteEncoder<MongoMessage> {

    private static final Logger log = LoggerFactory.getLogger(MongoWireMessageEncoder.class);

    @Override
    protected void encode(ChannelHandlerContext ctx, MongoMessage message, ByteBuf buf) {
        buf.writeIntLE(0); // write length later

        buf.writeIntLE(message.getHeader().getRequestID());
        buf.writeIntLE(message.getHeader().getResponseTo());
        buf.writeIntLE(OpCode.OP_MSG.getId());

        buf.writeIntLE(message.getFlags());
        buf.writeByte(MongoMessage.SECTION_KIND_BODY);

        Document document = message.getDocument();
        try {
            BsonEncoder.encodeDocument(document, buf);
        } catch (RuntimeException e) {
            log.error("Failed to encode {}", document, e);
            ctx.channel().close();
            throw e;
        }

        log.debug("wrote message: {}", message);

        // now set the length
        int writerIndex = buf.writerIndex();
        buf.setIntLE(0, writerIndex);
    }
}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/wire/MongoWireMessageEncoderTest.java
package de.bwaldvogel.mongo.wire;

import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import de.bwaldvogel.mongo.backend.Missing;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.wire.message.MessageHeader;
import de.bwaldvogel.mongo.wire.message.MongoMessage;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;

@ExtendWith(MockitoExtension.class)
public class MongoWireMessageEncoderTest {

    @Mock
    private ChannelHandlerContext ctx;

    @Mock
    private Channel channel;

    @BeforeEach
    void setUpContext() throws Exception {
        when(ctx.channel()).thenReturn(channel);
    }

    @Test
    void testExceptionHandling() throws Exception {
        MongoWireMessageEncoder mongoWireEncoder = new MongoWireMessageEncoder();

        MessageHeader header = new MessageHeader(0, 0);
        MongoMessage reply = new MongoMessage(channel, header, new Document("key", Missing.getInstance()));

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> mongoWireEncoder.encode(ctx, reply, Unpooled.buffer()))
            .withMessageContaining("Unexpected missing value. This must not happen. Please report a bug.");

        verify(channel).close();
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/MongoServer.java
package de.bwaldvogel.mongo;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.bwaldvogel.mongo.backend.Assert;
import de.bwaldvogel.mongo.wire.MongoDatabaseHandler;
import de.bwaldvogel.mongo.wire.MongoExceptionHandler;
import de.bwaldvogel.mongo.wire.MongoWireMessageEncoder;
import de.bwaldvogel.mongo.wire.MongoWireProtocolHandler;
import de.bwaldvogel.mongo.wire.MongoWireReplyEncoder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;

public class MongoServer {

    private static final Logger log = LoggerFactory.getLogger(MongoServer.class);

    private static final int DEFAULT_NETTY_EVENT_LOOP_THREADS = 0;

    private final MongoBackend backend;

    private EventLoopGroup bossGroup;

    private EventLoopGroup workerGroup;

    private ChannelGroup channelGroup;

    private Channel channel;

    private SslContext sslContext;

    public MongoServer(MongoBackend backend) {
        this.backend = backend;
    }

    public void enableOplog() {
        this.backend.enableOplog();
    }

    public void enableSsl(PrivateKey key, String keyPassword, X509Certificate... keyCertChain) {
        Assert.isNull(channel, () -> "Server already started");
        try {
            sslContext = SslContextBuilder.forServer(key, keyPassword, keyCertChain).build();
        } catch (SSLException e) {
            throw new RuntimeException("Failed to enable SSL", e);
        }
    }

    public void bind(String hostname, int port) {
        bind(new InetSocketAddress(hostname, port));
    }

    public void bind(SocketAddress socketAddress) {
        bind(socketAddress, DEFAULT_NETTY_EVENT_LOOP_THREADS, DEFAULT_NETTY_EVENT_LOOP_THREADS);
    }

    public void bind(SocketAddress socketAddress, int numberOfBossThreads, int numberOfWorkerThreads) {

        bossGroup = new NioEventLoopGroup(numberOfBossThreads, new MongoThreadFactory("mongo-server-boss"));
        workerGroup = new NioEventLoopGroup(numberOfWorkerThreads, new MongoThreadFactory("mongo-server-worker"));
        channelGroup = new DefaultChannelGroup("mongodb-channels", workerGroup.next());

        try {
            ServerBootstrap bootstrap = new ServerBootstrap()
                .group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 100)
                .localAddress(socketAddress)
                .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) throws Exception {
                        if (sslContext != null) {
                            ch.pipeline().addLast(sslContext.newHandler(ch.alloc()));
                        }
                        ch.pipeline().addLast(new MongoWireReplyEncoder());
                        ch.pipeline().addLast(new MongoWireMessageEncoder());
                        ch.pipeline().addLast(new MongoWireProtocolHandler());
                        ch.pipeline().addLast(new MongoDatabaseHandler(backend, channelGroup));
                        ch.pipeline().addLast(new MongoExceptionHandler());
                    }
                });

            channel = bootstrap.bind().syncUninterruptibly().channel();

            log.info("started {}", this);
        } catch (RuntimeException e) {
            shutdownNow();
            throw e;
        }
    }

    /**
     * Starts and binds the server on a local random port
     * <p>
     * Note: For modern clients you probably want to use {@link #bindAndGetConnectionString()} instead!
     *
     * @return the random local address the server was bound to
     */
    public InetSocketAddress bind() {
        bind(new InetSocketAddress("localhost", 0));
        return getLocalAddress();
    }

    /**
     * starts and binds the server on a local random port
     *
     * @return the MongoDB connection string to connect to this server. Example: mongodb://localhost:12345
     */
    public String bindAndGetConnectionString() {
        bind();
        return getConnectionString();
    }

    /**
     * @return the local address the server was bound or null if the server is not listening
     */
    public InetSocketAddress getLocalAddress() {
        if (channel == null) {
            return null;
        }
        return (InetSocketAddress) channel.localAddress();
    }

    /**
     * @return the MongoDB connection string to connect to this server. Example: mongodb://localhost:12345
     */
    public String getConnectionString() {
        final String options;
        if (sslContext != null) {
            options = "/?tls=true";
        } else {
            options = "";
        }

        InetSocketAddress socketAddress = getLocalAddress();
        return "mongodb://" + socketAddress.getHostString() + ":" + socketAddress.getPort() + options;
    }

    /**
     * Stop accepting new clients. Wait until all resources (such as client
     * connection) are closed and then shutdown. This method blocks until all
     * clients are finished. Use {@link #shutdownNow()} if the shutdown should
     * be forced.
     */
    public void shutdown() {
        stopListening();

        // Shut down all event loops to terminate all threads.
        if (bossGroup != null) {
            bossGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
        }
        if (workerGroup != null) {
            workerGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
        }

        if (bossGroup != null) {
            bossGroup.terminationFuture().syncUninterruptibly();
        }
        if (workerGroup != null) {
            workerGroup.terminationFuture().syncUninterruptibly();
        }

        backend.close();

        log.info("completed shutdown of {}", this);
    }

    /**
     * Closes the server socket. No new clients are accepted afterwards.
     */
    public void stopListening() {
        if (channel != null) {
            log.info("closing server channel");
            channel.close().syncUninterruptibly();
            channel = null;
        }
    }

    /**
     * Stops accepting new clients, closes all clients and finally shuts down
     * the server In contrast to {@link #shutdown()}, this method should not
     * block.
     */
    public void shutdownNow() {
        stopListening();
        closeClients();
        shutdown();
    }

    private void closeClients() {
        if (channelGroup != null) {
            int numClients = channelGroup.size();
            if (numClients > 0) {
                log.warn("Closing {} clients", numClients);
            }
            channelGroup.close().syncUninterruptibly();
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder(getClass().getSimpleName());
        sb.append("(");
        InetSocketAddress socketAddress = getLocalAddress();
        if (socketAddress != null) {
            sb.append("port: ").append(socketAddress.getPort());
            sb.append(", ssl: ").append(sslContext != null);
        }
        sb.append(")");
        return sb.toString();
    }

    /**
     * Use this method to simulate closing of cursors by the server, for instance due to timeout.
     */
    public void closeCursors(List<Long> cursorIds) {
        backend.closeCursors(cursorIds);
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/bson/Document.java
package de.bwaldvogel.mongo.bson;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import de.bwaldvogel.mongo.backend.Missing;

public final class Document implements Map<String, Object>, Bson {

    private static final long serialVersionUID = 1L;

    private final LinkedHashMap<String, Object> documentAsMap = new LinkedHashMap<>();

    public Document() {
    }

    public Document(String key, Object value) {
        this();
        append(key, value);
    }

    public Document(Map<String, ?> map) {
        this();
        putAll(map);
    }

    public void cloneInto(Document targetDocument) {
        for (Entry<String, Object> entry : entrySet()) {
            targetDocument.put(entry.getKey(), cloneDeeply(entry.getValue()));
        }
    }

    public Document cloneDeeply() {
        return cloneDeeply(this);
    }

    @SuppressWarnings("unchecked")
    private static <T> T cloneDeeply(T object) {
        if (object == null) {
            return null;
        } else if (object instanceof Document) {
            Document document = (Document) object;
            Document clone = document.clone();
            for (String key : document.keySet()) {
                clone.put(key, cloneDeeply(clone.get(key)));
            }
            return (T) clone;
        } else if (object instanceof List) {
            List<?> list = (List<?>) object;
            List<?> result = list.stream()
                .map(Document::cloneDeeply)
                .collect(Collectors.toList());
            return (T) result;
        } else if (object instanceof Set) {
            Set<?> set = (Set<?>) object;
            Set<?> result = set.stream()
                .map(Document::cloneDeeply)
                .collect(Collectors.toCollection(LinkedHashSet::new));
            return (T) result;
        } else {
            return object;
        }
    }

    public Document append(String key, Object value) {
        put(key, value);
        return this;
    }

    public Document appendAll(Map<String, Object> map) {
        putAll(map);
        return this;
    }

    @Override
    public boolean containsValue(Object value) {
        return documentAsMap.containsValue(value);
    }

    @Override
    public Object get(Object key) {
        return documentAsMap.get(key);
    }

    public Object getOrMissing(Object key) {
        return getOrDefault(key, Missing.getInstance());
    }

    @Override
    public void clear() {
        documentAsMap.clear();
    }

    @Override
    public int size() {
        return documentAsMap.size();
    }

    @Override
    public boolean isEmpty() {
        return documentAsMap.isEmpty();
    }

    @Override
    public boolean containsKey(Object key) {
        return documentAsMap.containsKey(key);
    }

    @Override
    public Object put(String key, Object value) {
        return documentAsMap.put(key, value);
    }

    public void putIfNotNull(String key, Object value) {
        if (value != null) {
            put(key, value);
        }
    }

    @Override
    public void putAll(Map<? extends String, ?> m) {
        documentAsMap.putAll(m);
    }

    @Override
    public Object remove(Object key) {
        return documentAsMap.remove(key);
    }

    @Override
    @SuppressWarnings("unchecked")
    public Document clone() {
        return new Document((Map<String, Object>) documentAsMap.clone());
    }

    @Override
    public Set<String> keySet() {
        return documentAsMap.keySet();
    }

    @Override
    public Collection<Object> values() {
        return documentAsMap.values();
    }

    @Override
    public Set<Entry<String, Object>> entrySet() {
        return documentAsMap.entrySet();
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        if (o == this) {
            return true;
        }
        if (!(o instanceof Document)) {
            return false;
        }
        List<String> keys = new ArrayList<>(keySet());
        List<String> otherKeys = new ArrayList<>(((Document) o).keySet());
        if (!keys.equals(otherKeys)) {
            return false;
        }
        return documentAsMap.equals(o);
    }

    @Override
    public int hashCode() {
        return documentAsMap.hashCode();
    }

    @Override
    public String toString() {
        return toString(false);
    }

    public String toString(boolean compactKey) {
        return toString(compactKey, "{", "}");
    }

    public String toString(boolean compactKey, String prefix, String suffix) {
        return documentAsMap.entrySet().stream()
            .map(entry -> writeKey(entry.getKey(), compactKey) + " " + Json.toJsonValue(entry.getValue(), compactKey, prefix, suffix))
            .collect(Collectors.joining(", ", prefix, suffix));
    }

    private String writeKey(String key, boolean compact) {
        if (compact) {
            return Json.escapeJson(key) + ":";
        } else {
            return "\"" + Json.escapeJson(key) + "\" :";
        }
    }

    public void merge(Document value) {
        putAll(value);
    }
}
<file_sep>/settings.gradle
include 'core'
include 'memory-backend'
include 'h2-backend'
include 'postgresql-backend'
include 'test-common'
include 'examples'

rootProject.children.each { it.name = rootProject.name + "-" + it.name }
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/stage/MatchStage.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import java.util.stream.Stream;

import de.bwaldvogel.mongo.backend.DefaultQueryMatcher;
import de.bwaldvogel.mongo.backend.QueryMatcher;
import de.bwaldvogel.mongo.bson.Document;

public class MatchStage implements AggregationStage {

    private final QueryMatcher queryMatcher = new DefaultQueryMatcher();
    private final Document query;

    public MatchStage(Document query) {
        this.query = query;
    }

    @Override
    public String name() {
        return "$match";
    }

    @Override
    public Stream<Document> apply(Stream<Document> stream) {
        return stream.filter(document -> queryMatcher.matches(document, query));
    }
}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/bson/LegacyUUIDTest.java
package de.bwaldvogel.mongo.bson;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.UUID;

import org.junit.jupiter.api.Test;

import nl.jqno.equalsverifier.EqualsVerifier;

class LegacyUUIDTest {

    @Test
    void testEqualsAndHashCode() throws Exception {
        EqualsVerifier.forClass(LegacyUUID.class)
            .withNonnullFields("uuid")
            .verify();
    }

    @Test
    void testCompare() throws Exception {
        UUID uuid0 = UUID.fromString("00000000-0000-0000-0000-000000000000");
        UUID uuid1 = UUID.fromString("48fe9251-9502-4d43-bd81-b8861eb69dc5");
        UUID uuid2 = UUID.fromString("cb6bdaa5-d134-4244-9383-ccded62a7bb3");
        UUID uuid3 = UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff");

        assertComparesLessThan(uuid0, uuid1);
        assertComparesLessThan(uuid2, uuid0);
        assertComparesLessThan(uuid2, uuid1);
        assertComparesLessThan(uuid2, uuid3);
        assertComparesLessThan(uuid3, uuid0);
    }

    private static void assertComparesLessThan(UUID a, UUID b) {
        assertComparesEqual(a);
        assertComparesEqual(b);
        assertThat(new LegacyUUID(a).compareTo(new LegacyUUID(b))).as(a + " < " + b).isEqualTo(-1);
        assertThat(new LegacyUUID(b).compareTo(new LegacyUUID(a))).as(b + " > " + a).isEqualTo(1);
    }

    private static void assertComparesEqual(UUID uuid) {
        assertThat(new LegacyUUID(uuid).compareTo(new LegacyUUID(uuid))).isEqualTo(0);
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/stage/LookupStage.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import static java.util.stream.Collectors.toList;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

import de.bwaldvogel.mongo.MongoCollection;
import de.bwaldvogel.mongo.MongoDatabase;
import de.bwaldvogel.mongo.backend.Utils;
import de.bwaldvogel.mongo.bson.Document;

public class LookupStage extends AbstractLookupStage {

    private static final String LOCAL_FIELD = "localField";
    private static final String FOREIGN_FIELD = "foreignField";

    private static final Set<String> CONFIGURATION_KEYS;
    static {
        CONFIGURATION_KEYS = new HashSet<>();
        CONFIGURATION_KEYS.add(FROM);
        CONFIGURATION_KEYS.add(LOCAL_FIELD);
        CONFIGURATION_KEYS.add(FOREIGN_FIELD);
        CONFIGURATION_KEYS.add(AS);
    }

    private final String localField;
    private final String foreignField;
    private final String as;

    private final MongoCollection<?> collection;

    public LookupStage(Document configuration, MongoDatabase mongoDatabase) {
        String from = readStringConfigurationProperty(configuration, FROM);
        collection = mongoDatabase.resolveCollection(from, false);
        localField = readStringConfigurationProperty(configuration, LOCAL_FIELD);
        foreignField = readStringConfigurationProperty(configuration, FOREIGN_FIELD);
        as = readStringConfigurationProperty(configuration, AS);
        ensureAllConfigurationPropertiesAreKnown(configuration, CONFIGURATION_KEYS);
    }

    @Override
    public Stream<Document> apply(Stream<Document> stream) {
        return stream.map(this::resolveRemoteField);
    }

    private Document resolveRemoteField(Document document) {
        Object value = Utils.getSubdocumentValue(document, localField);
        List<Document> documents = lookupValue(value);
        Document result = document.clone();
        result.put(as, documents);
        return result;
    }

    private List<Document> lookupValue(Object value) {
        if (collection == null) {
            return Collections.emptyList();
        }
        if (value instanceof List) {
            return ((List<?>) value).stream()
                .flatMap(item -> lookupValue(item).stream())
                .collect(toList());
        }
        Document query = new Document(foreignField, value);
        return collection.handleQueryAsStream(query).collect(toList());
    }
}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/DocumentComparatorTest.java
package de.bwaldvogel.mongo.backend;

import static de.bwaldvogel.mongo.TestUtils.json;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;

import de.bwaldvogel.mongo.bson.Document;

class DocumentComparatorTest {

    @Test
    void testCompareSingleKey() {
        DocumentComparator comparator = new DocumentComparator(json("a: 1"));

        List<Document> list = Stream.of(
                json("a: 10"),
                json("a: 15"),
                json("a: 5"),
                json("b: 1"))
            .sorted(comparator)
            .collect(Collectors.toList());

        assertThat(list).containsExactly(
            json("b: 1"),
            json("a: 5"),
            json("a: 10"),
            json("a: 15"));
    }

    @Test
    void testCompareMultiKey() {
        DocumentComparator comparator = new DocumentComparator(json("a: 1, b: -1"));

        List<Document> list = new ArrayList<>(List.of(
            json("a: 15, b: 3"),
            json("a: 15, b: 2"),
            json("a: 5"),
            json("b: 1"),
            json("b: 2"),
            json("b: 3")));

        Random rnd = new Random(4711);

        for (int i = 0; i < 10; i++) {
            Collections.shuffle(list, rnd);

            list.sort(comparator);
            assertThat(list).containsExactly(
                json("b: 3"),
                json("b: 2"),
                json("b: 1"),
                json("a: 5"),
                json("a: 15, b: 3"),
                json("a: 15, b: 2"));
        }
    }

    @Test
    void testCompareCompoundKey() throws Exception {
        DocumentComparator comparator = new DocumentComparator(json("'a.b': 1, c: -1"));

        Document a = json("a: {b: 10}");
        Document b = json("a: {b: 15}");
        Document c = json("a: {b: 15, x: 70}");

        assertThat(comparator.compare(a, b)).isLessThan(0);
        assertThat(comparator.compare(b, a)).isGreaterThan(0);
        assertThat(comparator.compare(b, c)).isZero();

    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/Index.java
package de.bwaldvogel.mongo.backend;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import de.bwaldvogel.mongo.MongoCollection;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.CannotIndexParallelArraysError;
import de.bwaldvogel.mongo.exception.KeyConstraintError;

public abstract class Index<P> {

    private final String name;
    private final List<IndexKey> keys;
    private final boolean sparse;

    protected Index(String name, List<IndexKey> keys, boolean sparse) {
        this.name = name;
        this.keys = keys;
        this.sparse = sparse;
    }

    protected boolean isSparse() {
        return sparse;
    }

    public List<IndexKey> getKeys() {
        return keys;
    }

    public boolean hasSameOptions(Index<?> other) {
        return sparse == other.sparse;
    }

    public String getName() {
        return name;
    }

    protected List<String> keys() {
        return keys.stream()
            .map(IndexKey::getKey)
            .collect(Collectors.toList());
    }

    protected Set<String> keySet() {
        return keys.stream()
            .map(IndexKey::getKey)
            .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    public Set<KeyValue> getKeyValues(Document document) {
        return getKeyValues(document, true);
    }

    Set<KeyValue> getKeyValues(Document document, boolean normalize) {
        Map<String, Object> valuesPerKey = collectValuesPerKey(document);
        if (normalize) {
            valuesPerKey.replaceAll((key, value) -> Utils.normalizeValue(value));
        }

        List<Collection<Object>> collectionValues = valuesPerKey.values().stream()
            .filter(value -> value instanceof Collection)
            .map(value -> (Collection<Object>) value)
            .collect(Collectors.toList());

        if (collectionValues.size() > 0) {
            validateHasNoParallelArrays(document);
            return CollectionUtils.multiplyWithOtherElements(valuesPerKey.values(), collectionValues).stream()
                .map(KeyValue::new)
                .collect(StreamUtils.toLinkedHashSet());
        } else {
            return Set.of(new KeyValue(valuesPerKey.values()));
        }
    }

    private void validateHasNoParallelArrays(Document document) {
        Set<List<String>> arrayPaths = new LinkedHashSet<>();
        for (String key : keys()) {
            List<String> pathToFirstCollection = getPathToFirstCollection(document, key);
            if (pathToFirstCollection != null) {
                arrayPaths.add(pathToFirstCollection);
            }
        }
        if (arrayPaths.size() > 1) {
            List<String> parallelArraysPaths = arrayPaths.stream()
                .map(path -> path.get(path.size() - 1))
                .collect(Collectors.toList());
            throw new CannotIndexParallelArraysError(parallelArraysPaths);
        }
    }

    private static List<String> getPathToFirstCollection(Document document, String key) {
        List<String> fragments = Utils.splitPath(key);
        List<String> remainingFragments = Utils.getTail(fragments);
        return getPathToFirstCollection(document, remainingFragments, List.of(fragments.get(0)));
    }

    private static List<String> getPathToFirstCollection(Document document, List<String> remainingFragments, List<String> path) {
        Object value = Utils.getSubdocumentValue(document, Utils.joinPath(path));
        if (value instanceof Collection) {
            return path;
        }
        if (remainingFragments.isEmpty()) {
            return null;
        }
        List<String> newPath = new ArrayList<>(path);
        newPath.add(remainingFragments.get(0));
        return getPathToFirstCollection(document, Utils.getTail(remainingFragments), newPath);
    }

    private Map<String, Object> collectValuesPerKey(Document document) {
        Map<String, Object> valuesPerKey = new LinkedHashMap<>();
        for (String key : keys()) {
            Object value = Utils.getSubdocumentValueCollectionAware(document, key);
            valuesPerKey.put(key, value);
        }
        return valuesPerKey;
    }

    public abstract P getPosition(Document document);

    public abstract void checkAdd(Document document, MongoCollection<P> collection);

    public abstract void add(Document document, P position, MongoCollection<P> collection);

    public abstract P remove(Document document);

    public abstract boolean canHandle(Document query);

    public abstract Iterable<P> getPositions(Document query);

    public abstract long getCount();

    public boolean isEmpty() {
        return getCount() == 0;
    }

    public abstract long getDataSize();

    public abstract void checkUpdate(Document oldDocument, Document newDocument, MongoCollection<P> collection);

    public abstract void updateInPlace(Document oldDocument, Document newDocument, P position, MongoCollection<P> collection) throws KeyConstraintError;

    protected boolean isCompoundIndex() {
        return keys().size() > 1;
    }

    protected boolean nullAwareEqualsKeys(Document oldDocument, Document newDocument) {
        Set<KeyValue> oldKeyValues = getKeyValues(oldDocument);
        Set<KeyValue> newKeyValues = getKeyValues(newDocument);
        return Utils.nullAwareEquals(oldKeyValues, newKeyValues);
    }

    public abstract void drop();

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[name=" + getName() + "]";
    }

    public boolean isUnique() {
        return false;
    }

    public Document toIndexDescription() {
        Document indexDescription = new Document("v", 2)
            .append("unique", isUnique());

        Document key = new Document();
        for (IndexKey indexKey : getKeys()) {
            key.put(indexKey.getKey(), indexKey.isAscending() ? 1 : -1);
        }

        indexDescription.put("key", key);

        indexDescription.put("name", getName());

        if (isSparse()) {
            indexDescription.put("sparse", true);
        }

        return indexDescription;
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/stage/BucketStage.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import de.bwaldvogel.mongo.backend.Assert;
import de.bwaldvogel.mongo.backend.CollectionUtils;
import de.bwaldvogel.mongo.backend.Constants;
import de.bwaldvogel.mongo.backend.Missing;
import de.bwaldvogel.mongo.backend.Utils;
import de.bwaldvogel.mongo.backend.ValueComparator;
import de.bwaldvogel.mongo.backend.aggregation.Expression;
import de.bwaldvogel.mongo.backend.aggregation.accumulator.Accumulator;
import de.bwaldvogel.mongo.backend.aggregation.accumulator.SumAccumulator;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.bson.Json;
import de.bwaldvogel.mongo.exception.MongoServerError;

public class BucketStage implements AggregationStage {

    private final Object groupByExpression;
    private final List<?> boundaries;
    private final Object defaultValue;
    private final Document output;

    public BucketStage(Document document) {
        groupByExpression = validateGroupBy(document.get("groupBy"));
        boundaries = getAndValidateBoundaries(document.get("boundaries"));
        defaultValue = getAndValidateDefault(document.getOrMissing("default"));
        output = getAndValidateOutput(document.get("output"));
    }

    @Override
    public String name() {
        return "$bucket";
    }

    private void validateValuePresent(Object value) {
        if (value == null) {
            throw new MongoServerError(40198, "$bucket requires 'groupBy' and 'boundaries' to be specified.");
        }
    }

    private Object validateGroupBy(Object groupBy) {
        validateValuePresent(groupBy);
        if (!(groupBy instanceof String || groupBy instanceof Document)) {
            String value = Json.toJsonValue(groupBy);
            throw new MongoServerError(40202, "The $bucket 'groupBy' field must be defined as a $-prefixed path or an expression, but found: " + value + ".");
        }
        return groupBy;
    }

    private List<?> getAndValidateBoundaries(Object boundaries) {
        validateValuePresent(boundaries);
        if (!(boundaries instanceof List)) {
            String type = Utils.describeType(boundaries);
            throw new MongoServerError(40200, "The $bucket 'boundaries' field must be an array, but found type: " + type + ".");
        }
        List<?> boundaryValues = (List<?>) boundaries;
        if (boundaryValues.size() < 2) {
            throw new MongoServerError(40192, "The $bucket 'boundaries' field must have at least 2 values, but found " + boundaryValues.size() + " value(s).");
        }

        for (int i = 1; i < boundaryValues.size(); i++) {
            Object value1 = boundaryValues.get(i - 1);
            Object value2 = boundaryValues.get(i);

            validateTypesAreCompatible(value1, value2);

            if (compare(value1, value2) >= 0) {
                int index1 = i - 1;
                int index2 = i;
                throw new MongoServerError(40194, "The 'boundaries' option to $bucket must be sorted, but elements " + index1 + " and " + index2 + " are not in ascending order (" + value1 + " is not less than " + value2 + ").");
            }
        }

        return boundaryValues;
    }

    private Object getAndValidateDefault(Object defaultValue) {
        Assert.notEmpty(boundaries);
        if (!(compare(defaultValue, boundaries.get(0)) < 0
            || compare(defaultValue, CollectionUtils.getLastElement(boundaries)) >= 0)) {
            throw new MongoServerError(40199, "The $bucket 'default' field must be less than the lowest boundary or greater than or equal to the highest boundary.");
        }
        return defaultValue;
    }

    private void validateTypesAreCompatible(Object value1, Object value2) {
        if (value1 instanceof Number && value2 instanceof Number) {
            return;
        }
        String type1 = Utils.describeType(value1);
        String type2 = Utils.describeType(value2);
        if (!type1.equals(type2)) {
            throw new MongoServerError(40193, "All values in the the 'boundaries' option to $bucket must have the same type. Found conflicting types " + type1 + " and " + type2 + ".");
        }
    }

    private Document getAndValidateOutput(Object output) {
        if (output == null) {
            return null;
        }
        if (!(output instanceof Document)) {
            throw new MongoServerError(40196, "The $bucket 'output' field must be an object, but found type: " + Utils.describeType(output) + ".");
        }
        return (Document) output;
    }

    @Override
    public Stream<Document> apply(Stream<Document> stream) {
        Map<Object, List<Accumulator>> accumulatorsPerBucket = new TreeMap<>(ValueComparator.asc());
        stream.forEach(document -> {
            Object key = Expression.evaluateDocument(groupByExpression, document);
            Object bucket = findBucket(key);
            List<Accumulator> accumulators = accumulatorsPerBucket.computeIfAbsent(bucket, k -> getAccumulators());

            for (Accumulator accumulator : accumulators) {
                Object expression = accumulator.getExpression();
                accumulator.aggregate(Expression.evaluateDocument(expression, document));
            }
        });
        List<Document> result = new ArrayList<>();
        for (Entry<Object, List<Accumulator>> entry : accumulatorsPerBucket.entrySet()) {
            Document groupResult = new Document();
            groupResult.put(Constants.ID_FIELD, entry.getKey());

            for (Accumulator accumulator : entry.getValue()) {
                groupResult.put(accumulator.getField(), accumulator.getResult());
            }

            result.add(groupResult);
        }

        return result.stream();
    }

    private List<Accumulator> getAccumulators() {
        if (output == null) {
            return List.of(new SumAccumulator("count", new Document("$sum", 1)));
        }
        return Accumulator.parse(output).values().stream()
            .map(Supplier::get)
            .collect(Collectors.toList());
    }

    private Object findBucket(Object key) {
        if (compare(key, boundaries.get(0)) < 0) {
            return getDefaultValue();
        }
        for (int i = 1; i < boundaries.size(); i++) {
            Object boundary = boundaries.get(i);
            if (compare(key, boundary) < 0) {
                return boundaries.get(i - 1);
            }
        }
        return getDefaultValue();
    }

    private static int compare(Object a, Object b) {
        return ValueComparator.asc().compare(a, b);
    }

    private Object getDefaultValue() {
        if (defaultValue instanceof Missing) {
            throw new MongoServerError(40066, "$switch could not find a matching branch for an input, and no default was specified.");
        }
        return defaultValue;
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/MongoServerNotYetImplementedException.java
package de.bwaldvogel.mongo.exception;

public class MongoServerNotYetImplementedException extends MongoServerException {

    private static final long serialVersionUID = 1L;

    private static final String ISSUES_URL = "https://github.com/bwaldvogel/mongo-java-server/issues/";

    public MongoServerNotYetImplementedException(int gitHubIssueNumber, String prefix) {
        super(prefix + " is not yet implemented. See " + ISSUES_URL + gitHubIssueNumber);
    }

}
<file_sep>/postgresql-backend/src/main/java/de/bwaldvogel/mongo/backend/postgresql/LegacyUUIDJsonMixIn.java
package de.bwaldvogel.mongo.backend.postgresql;

import java.util.UUID;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

abstract class LegacyUUIDJsonMixIn {

    @JsonCreator
    LegacyUUIDJsonMixIn(@JsonProperty("uuid") UUID uuid) {}

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/InMemoryCursor.java
package de.bwaldvogel.mongo.backend;

import java.util.Collections;
import java.util.List;

import de.bwaldvogel.mongo.bson.Document;

public class InMemoryCursor extends AbstractCursor {

    private List<Document> remainingDocuments;

    public InMemoryCursor(long cursorId, List<Document> remainingDocuments) {
        super(cursorId);
        Assert.notEmpty(remainingDocuments);
        this.remainingDocuments = Collections.unmodifiableList(remainingDocuments);
    }

    @Override
    public boolean isEmpty() {
        return remainingDocuments.isEmpty();
    }

    @Override
    public List<Document> takeDocuments(int numberToReturn) {
        Assert.isTrue(numberToReturn > 0, () -> "Illegal number to return: " + numberToReturn);
        int toIndex = Math.min(remainingDocuments.size(), numberToReturn);
        List<Document> documents = remainingDocuments.subList(0, toIndex);
        remainingDocuments = remainingDocuments.subList(documents.size(), remainingDocuments.size());
        return documents;
    }

}
<file_sep>/test-common/src/test/java/de/bwaldvogel/mongo/RealMongoAggregationTest.java
package de.bwaldvogel.mongo;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import de.bwaldvogel.mongo.backend.AbstractAggregationTest;

public class RealMongoAggregationTest extends AbstractAggregationTest {

    @RegisterExtension
    static RealMongoContainer realMongoContainer = new RealMongoContainer();

    @Override
    protected void setUpBackend() throws Exception {
        connectionString = realMongoContainer.getConnectionString();
    }

    @Override
    protected MongoBackend createBackend() throws Exception {
        throw new UnsupportedOperationException();
    }

    @Test
    @Override
    @Disabled
    public void testAggregateWithGeoNear() throws Exception {
        super.testAggregateWithGeoNear();
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/ArrayFilters.java
package de.bwaldvogel.mongo.backend;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.bson.Json;
import de.bwaldvogel.mongo.exception.BadValueException;
import de.bwaldvogel.mongo.exception.FailedToParseException;

public class ArrayFilters {

    private final Map<String, Object> values;

    private ArrayFilters(Map<String, Object> values) {
        this.values = values;
    }

    static ArrayFilters parse(Document query, Document updateQuery) {
        @SuppressWarnings("unchecked")
        List<Document> arrayFilters = (List<Document>) query.getOrDefault("arrayFilters", Collections.emptyList());
        return parse(arrayFilters, updateQuery);
    }

    private static ArrayFilters parse(List<Document> arrayFilters, Document updateQuery) {
        Map<String, Object> arrayFilterMap = new LinkedHashMap<>();
        for (Document arrayFilter : arrayFilters) {
            if (arrayFilter.isEmpty()) {
                throw new FailedToParseException("Cannot use an expression without a top-level field name in arrayFilters");
            }

            List<String> topLevelFieldNames = arrayFilter.keySet().stream()
                .map(Utils::firstFragment)
                .distinct()
                .collect(Collectors.toList());

            if (topLevelFieldNames.size() > 1) {
                throw new FailedToParseException("Error parsing array filter :: caused by ::" +
                    " Expected a single top-level field name, found '" + topLevelFieldNames.get(0) + "' and '" + topLevelFieldNames.get(1) + "'");
            }

            String topLevelFieldName = CollectionUtils.getSingleElement(topLevelFieldNames);
            if (!topLevelFieldName.matches("^[a-zA-Z0-9]+$")) {
                throw new BadValueException("Error parsing array filter :: caused by :: The top-level field name must be an alphanumeric string beginning with a lowercase letter, found '" + topLevelFieldName + "'");
            }

            Object filter = createFilter(arrayFilter);
            if (arrayFilterMap.put(topLevelFieldName, filter) != null) {
                throw new FailedToParseException("Found multiple array filters with the same top-level field name " + topLevelFieldName);
            }
        }

        if (!arrayFilterMap.isEmpty()) {
            validate(updateQuery, arrayFilterMap);
        }

        return new ArrayFilters(arrayFilterMap);
    }

    private static Object createFilter(Document arrayFilter) {
        Document filter = new Document();

        for (Entry<String, Object> entry : arrayFilter.entrySet()) {
            List<String> pathFragments = Utils.splitPath(entry.getKey());
            String tailPath = Utils.joinTail(pathFragments);
            Object query = entry.getValue();
            if (tailPath.isEmpty()) {
                Assert.hasSize(arrayFilter.keySet(), 1);
                return query;
            } else {
                filter.put(tailPath, query);
            }
        }
        return filter;
    }

    private static Object createFilter(List<String> pathFragments, Object query) {
        List<String> tail = Utils.getTail(pathFragments);
        if (tail.isEmpty()) {
            return query;
        } else {
            return new Document(Utils.joinPath(tail), query);
        }
    }

    private static void validate(Document updateQuery, Map<String, ?> arrayFilterMap) {
        Set<String> allKeys = updateQuery.values().stream()
            .filter(Document.class::isInstance)
            .map(Document.class::cast)
            .flatMap(d -> d.keySet().stream())
            .collect(Collectors.toSet());

        for (String identifier : arrayFilterMap.keySet()) {
            if (allKeys.stream().noneMatch(key -> key.contains(toPositionalOperator(identifier)))) {
                throw new FailedToParseException("The array filter for identifier '" + identifier
                    + "' was not used in the update " + updateQuery.toString(true, "{ ", " }"));
            }
        }
    }

    public static ArrayFilters empty() {
        return new ArrayFilters(Collections.emptyMap());
    }

    @VisibleForExternalBackends
    public boolean isEmpty() {
        return getValues().isEmpty();
    }

    @Override
    public String toString() {
        return Json.toJsonValue(getValues());
    }

    private Object getArrayFilterQuery(String key) {
        if (isPositionalAll(key)) {
            return new Document(QueryOperator.EXISTS.getValue(), true);
        }
        return values.get(extractKeyFromPositionalOperator(key));
    }

    private static String toPositionalOperator(String key) {
        return "$[" + key + "]";
    }

    private static String extractKeyFromPositionalOperator(String operator) {
        if (!isPositionalOperator(operator)) {
            throw new IllegalArgumentException("Illegal key: " + operator);
        }
        return operator.substring("$[".length(), operator.length() - "]".length());
    }

    private static boolean isPositionalOperator(String key) {
        return key.startsWith("$[") && key.endsWith("]");
    }

    List<String> calculateKeys(Document document, String key) {
        if (isPositionalOperator(key)) {
            throw new BadValueException("Cannot have array filter identifier (i.e. '$[<id>]') element in the first position in path '" + key + "'", false);
        }

        List<String> pathFragments = Utils.splitPath(key);
        return calculateKeys(document, pathFragments, "");
    }

    private List<String> calculateKeys(Object object, List<String> pathFragments, String path) {
        if (pathFragments.isEmpty()) {
            return List.of(path);
        }
        String fragment = pathFragments.get(0);

        if (!isPositionalOperator(fragment)) {
            String nextPath = Utils.joinPath(path, fragment);
            List<String> tail = Utils.getTail(pathFragments);
            Object subObject = Utils.getFieldValueListSafe(object, fragment);
            return calculateKeys(subObject, tail, nextPath);
        }

        if (object instanceof Missing) {
            throw new BadValueException("The path '" + path + "' must exist in the document in order to apply array updates.");
        } else if (!(object instanceof List)) {
            String previousKey = Utils.getLastFragment(path);
            String element = Json.toCompactJsonValue(object);
            throw new BadValueException("Cannot apply array updates to non-array element " + previousKey + ": " + element);
        }

        List<?> values = (List<?>) object;
        Object arrayFilterQuery = getArrayFilterQuery(fragment);
        QueryMatcher queryMatcher = new DefaultQueryMatcher();
        List<String> keys = new ArrayList<>();
        for (int i = 0; i < values.size(); i++) {
            Object value = values.get(i);
            if (queryMatcher.matchesValue(arrayFilterQuery, value)) {
                List<String> remaining = Utils.getTail(pathFragments);
                String nextPath = Utils.joinPath(path, String.valueOf(i));
                List<String> subKeys = calculateKeys(value, remaining, nextPath);
                keys.addAll(subKeys);
            }
        }

        return keys;
    }

    private static boolean isPositionalAll(String key) {
        return key.equals("$[]");
    }

    Map<String, Object> getValues() {
        return values;
    }

    boolean canHandle(String key) {
        if (!isEmpty()) {
            return true;
        } else {
            return Utils.splitPath(key).stream().anyMatch(ArrayFilters::isPositionalAll);
        }
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/CursorRegistry.java
package de.bwaldvogel.mongo.backend;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

import de.bwaldvogel.mongo.exception.CursorNotFoundException;

public class CursorRegistry {

    private final ConcurrentMap<Long, Cursor> cursors = new ConcurrentHashMap<>();
    private final AtomicLong cursorIdCounter = new AtomicLong();

    public long generateCursorId() {
        return cursorIdCounter.incrementAndGet();
    }

    public Cursor getCursor(long cursorId) {
        Cursor cursor = cursors.get(cursorId);
        if (cursor == null) {
            throw new CursorNotFoundException(cursorId);
        }
        return cursor;
    }

    public boolean remove(Cursor cursor) {
        return remove(cursor.getId());
    }

    public boolean remove(long cursorId) {
        return cursors.remove(cursorId) != null;
    }

    public void add(Cursor cursor) {
        Cursor previousValue = cursors.put(cursor.getId(), cursor);
        Assert.isNull(previousValue);
    }

    public int size() {
        return cursors.size();
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/NoReplicationEnabledException.java
package de.bwaldvogel.mongo.exception;

public class NoReplicationEnabledException extends MongoServerError {

    private static final long serialVersionUID = 1L;

    public NoReplicationEnabledException() {
        super(76, "NoReplicationEnabled", "not running with --replSet");
        setLogError(false);
    }

}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/wire/bson/BsonDecoderTest.java
package de.bwaldvogel.mongo.wire.bson;

import static org.assertj.core.api.Assertions.assertThat;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;

import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.bson.MaxKey;
import de.bwaldvogel.mongo.bson.MinKey;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

public class BsonDecoderTest {

    @Test
    void testDecodeStringUnicode() throws Exception {
        String string = "\u0442\u0435\u0441\u0442";
        byte[] bytes = string.getBytes(StandardCharsets.UTF_8);
        ByteBuf buffer = Unpooled.buffer();
        try {
            buffer.writeBytes(bytes);
            buffer.writeByte(0);
            assertThat(BsonDecoder.decodeCString(buffer)).isEqualTo(string);
        } finally {
            buffer.release();
        }
    }

    @Test
    void testEncodeDecodeRoundtrip() throws Exception {
        List<Document> objects = new ArrayList<>();
        objects.add(new Document("key", MaxKey.getInstance()).append("foo", "bar"));
        objects.add(new Document("key", MinKey.getInstance()).append("test", MaxKey.getInstance()));

        for (Document document : objects) {
            ByteBuf buffer = Unpooled.buffer();
            try {
                BsonEncoder.encodeDocument(document, buffer);
                Document decodedObject = BsonDecoder.decodeBson(buffer);
                assertThat(decodedObject).isEqualTo(document);
            } finally {
                buffer.release();
            }
        }
    }
}
<file_sep>/memory-backend/build.gradle
dependencies {
    api project(':mongo-java-server-core')
    testImplementation project(':mongo-java-server-test-common')
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/KeyConstraintError.java
package de.bwaldvogel.mongo.exception;

public class KeyConstraintError extends MongoServerError {

    private static final long serialVersionUID = 1L;

    KeyConstraintError(ErrorCode errorCode, String message) {
        super(errorCode, message);
    }
}
<file_sep>/test-common/src/main/java/de/bwaldvogel/mongo/backend/TestUtils.java
package de.bwaldvogel.mongo.backend;

import java.net.InetSocketAddress;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.bson.Document;

import com.mongodb.ConnectionString;
import com.mongodb.client.MongoDatabase;

public class TestUtils {

    private TestUtils() {
    }

    public static <T> List<T> toArray(Iterable<T> iterable) {
        List<T> array = new ArrayList<>();
        for (T obj : iterable) {
            array.add(obj);
        }
        return array;
    }

    public static Document json(String string) {
        string = string.trim();
        if (!string.startsWith("{")) {
            string = "{" + string + "}";
        }
        return Document.parse(string);
    }

    public static List<Document> jsonList(String... json) {
        return Stream.of(json)
            .map(TestUtils::json)
            .collect(Collectors.toList());
    }

    public static Document getCollectionStatistics(MongoDatabase database, String collectionName) {
        Document collStats = new Document("collStats", collectionName);
        return database.runCommand(collStats);
    }

    static Instant instant(String value) {
        return Instant.parse(value);
    }

    static Date date(String value) {
        return Date.from(instant(value));
    }

    public static InetSocketAddress toInetSocketAddress(String connectionString) {
        return toInetSocketAddress(new ConnectionString(connectionString));
    }

    public static InetSocketAddress toInetSocketAddress(ConnectionString connectionString) {
        String hostAndPort = CollectionUtils.getSingleElement(connectionString.getHosts());
        String[] hostAndPortArray = hostAndPort.split(":");
        String hostname = hostAndPortArray[0];
        int port = Integer.parseInt(hostAndPortArray[1]);
        return new InetSocketAddress(hostname, port);
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/wire/message/ClientRequest.java
package de.bwaldvogel.mongo.wire.message;

import de.bwaldvogel.mongo.backend.Utils;
import io.netty.channel.Channel;

public abstract class ClientRequest implements Message {

    private final MessageHeader header;
    private final String fullCollectionName;
    private final Channel channel;

    protected ClientRequest(Channel channel, MessageHeader header, String fullCollectionName) {
        this.channel = channel;
        this.header = header;
        this.fullCollectionName = fullCollectionName;
    }

    public Channel getChannel() {
        return channel;
    }

    public MessageHeader getHeader() {
        return header;
    }

    @Override
    public String getDatabaseName() {
        return Utils.getDatabaseNameFromFullName(fullCollectionName);
    }

    public String getCollectionName() {
        return Utils.getCollectionNameFromFullName(fullCollectionName);
    }

    public String getFullCollectionName() {
        return fullCollectionName;
    }
}
<file_sep>/examples/src/main/java/SimpleJUnit5WithModernClientTest.java
import static org.assertj.core.api.Assertions.assertThat;

import org.bson.Document;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;

import de.bwaldvogel.mongo.MongoServer;
import de.bwaldvogel.mongo.backend.memory.MemoryBackend;

class SimpleJUnit5WithModernClientTest {

    private MongoCollection<Document> collection;
    private MongoClient client;
    private MongoServer server;

    @BeforeEach
    void setUp() {
        server = new MongoServer(new MemoryBackend());

        // bind on a random local port
        String connectionString = server.bindAndGetConnectionString();

        client = MongoClients.create(connectionString);
        collection = client.getDatabase("testdb").getCollection("testcollection");
    }

    @AfterEach
    void tearDown() {
        client.close();
        server.shutdown();
    }

    @Test
    void testSimpleInsertQuery() throws Exception {
        assertThat(collection.countDocuments()).isZero();

        // creates the database and collection in memory and insert the object
        Document obj = new Document("_id", 1).append("key", "value");
        collection.insertOne(obj);

        assertThat(collection.countDocuments()).isEqualTo(1L);
        assertThat(collection.find().first()).isEqualTo(obj);
    }

}
<file_sep>/h2-backend/build.gradle
dependencies {
    api project(':mongo-java-server-core')

    api group: 'com.h2database', name: 'h2', version: 'latest.release'

    testImplementation project(':mongo-java-server-test-common')
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/LimitedList.java
package de.bwaldvogel.mongo.backend;

import java.util.ArrayList;

public class LimitedList<E> extends ArrayList<E> {

    private static final long serialVersionUID = -4265811949513159615L;

    private final int limit;

    LimitedList(int limit) {
        this.limit = limit;
    }

    @Override
    public boolean add(E o) {
        super.add(o);
        while (size() > limit) {
            super.remove(0);
        }
        return true;
    }
}
<file_sep>/docker-compose.yml
version: '2.1'

services:
  mongodb:
    image: mongo:5.0.18
    container_name: mongo-java-server-test
    tmpfs:
      - /data:rw
    ports:
      - 127.0.0.1:27018:27017
    healthcheck:
      test: echo 'db.runCommand("ping").ok' | mongo mongo:27017/test --quiet 1
      interval: 10s
      timeout: 5s
      retries: 5

  postgres:
    image: postgres:9.6-alpine
    container_name: postgres-mongo-java-server-test
    tmpfs:
      - /var/lib/postgresql/data:rw
    environment:
      - POSTGRES_USER=mongo-java-server-test
      - POSTGRES_PASSWORD=<PASSWORD>
      - POSTGRES_DB=mongo-java-server-test
    ports:
      - 127.0.0.1:5432:5432
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  sonarqube:
    image: sonarqube
    container_name: mongo-java-server-sonarqube
    ports:
      - 127.0.0.1:9000:9000
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/bson/BinData.java
package de.bwaldvogel.mongo.bson;

import java.util.Arrays;
import java.util.Objects;

public final class BinData implements Comparable<BinData>, Bson {

    private static final long serialVersionUID = 1L;

    private final byte[] data;

    public BinData(byte[] data) {
        this.data = Objects.requireNonNull(data);
    }

    public byte[] getData() {
        return data;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        BinData binData = (BinData) o;
        return Arrays.equals(data, binData.data);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(data);
    }

    @Override
    public int compareTo(BinData other) {
        byte[] bytes1 = getData();
        byte[] bytes2 = other.getData();
        if (bytes1.length != bytes2.length) {
            return Integer.compare(bytes1.length, bytes2.length);
        } else {
            for (int i = 0; i < bytes1.length; i++) {
                int compare = compareUnsigned(bytes1[i], bytes2[i]);
                if (compare != 0) return compare;
            }
            return 0;
        }
    }

    // lexicographic byte comparison 0x00 < 0xFF
    private static int compareUnsigned(byte b1, byte b2) {
        int v1 = (int) b1 & 0xFF;
        int v2 = (int) b2 & 0xFF;
        return Integer.compare(v1, v2);
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/CursorNotFoundException.java
package de.bwaldvogel.mongo.exception;

public class CursorNotFoundException extends MongoServerError {

    private static final long serialVersionUID = 1L;

    public CursorNotFoundException(long cursorId) {
        super(ErrorCode.CursorNotFound, "Cursor id " + cursorId + " does not exist");
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/wire/message/Message.java
package de.bwaldvogel.mongo.wire.message;

public interface Message {

    String getDatabaseName();

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/BsonType.java
package de.bwaldvogel.mongo.backend;

import java.time.Instant;
import java.util.Collection;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;

import de.bwaldvogel.mongo.bson.BinData;
import de.bwaldvogel.mongo.bson.BsonTimestamp;
import de.bwaldvogel.mongo.bson.Decimal128;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.bson.LegacyUUID;
import de.bwaldvogel.mongo.bson.MaxKey;
import de.bwaldvogel.mongo.bson.MinKey;
import de.bwaldvogel.mongo.bson.ObjectId;
import de.bwaldvogel.mongo.exception.BadValueException;

public enum BsonType {

    DOUBLE(1, "double", Double.class),
    STRING(2, "string", String.class),
    OBJECT(3, "object", Document.class),
    ARRAY(4, "array", Collection.class),
    BIN_DATA(5, "binData", BinData.class, LegacyUUID.class, UUID.class),
    OBJECT_ID(7, "objectId", ObjectId.class),
    BOOL(8, "bool", Boolean.class),
    DATE(9, "date", Instant.class),
    NULL(10, "null") {
        @Override
        public boolean matches(Object value) {
            return value == null;
        }
    },
    REGEX(11, "regex", Pattern.class),
    INT(16, "int", Integer.class),
    TIMESTAMP(17, "timestamp", BsonTimestamp.class),
    LONG(18, "long", Long.class),
    DECIMAL128(19, "decimal", Decimal128.class),
    MIN_KEY(-1, "minKey", MinKey.class),
    MAX_KEY(127, "maxKey", MaxKey.class),
    ;

    private final int number;
    private final String alias;
    private final Set<Class<?>> classes;

    BsonType(int number, String alias, Class<?>... classes) {
        this.number = number;
        this.alias = alias;
        this.classes = Set.of(classes);
    }

    public static BsonType forString(String value) {
        for (BsonType bsonType : values()) {
            if (bsonType.getAlias().equals(value)) {
                return bsonType;
            }
        }
        throw new BadValueException("Unknown type name alias: " + value);
    }

    public static BsonType forNumber(Number value) {
        int type = value.intValue();

        if ((double) type != value.doubleValue()) {
            throw new BadValueException("Invalid numerical type code: " + value);
        }
        for (BsonType bsonType : values()) {
            if (bsonType.getNumber() == type) {
                return bsonType;
            }
        }
        throw new IllegalArgumentException("Unknown type: " + value);
    }

    public boolean matches(Object value) {
        if (value == null) {
            return false;
        }
        return classes.stream().anyMatch(clazz -> clazz.isAssignableFrom(value.getClass()));
    }

    public int getNumber() {
        return number;
    }

    public String getAlias() {
        return alias;
    }
}
<file_sep>/test-common/src/main/java/de/bwaldvogel/mongo/entity/SubEntity.java
package de.bwaldvogel.mongo.entity;

public class SubEntity {

    private String data;

    public SubEntity() {
    }

    public SubEntity(String data) {
        this.data = data;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}
<file_sep>/postgresql-backend/src/test/java/de/bwaldvogel/mongo/backend/postgresql/PostgresqlUtilsTest.java
package de.bwaldvogel.mongo.backend.postgresql;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;

import org.junit.jupiter.api.Test;

import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.bson.ObjectId;

class PostgresqlUtilsTest {

    @Test
    void testToDataKey() throws Exception {
        assertThat(PostgresqlUtils.toDataKey("foo")).isEqualTo("data ->> 'foo'");
        assertThat(PostgresqlUtils.toDataKey("foo.bar")).isEqualTo("data -> 'foo' ->> 'bar'");
        assertThat(PostgresqlUtils.toDataKey("foo.bar.bla")).isEqualTo("data -> 'foo' -> 'bar' ->> 'bla'");
    }

    @Test
    void testToQueryValue() throws Exception {
        assertThat(PostgresqlUtils.toQueryValue(123)).isEqualTo("123");
        assertThat(PostgresqlUtils.toQueryValue(123.0)).isEqualTo("123");
        assertThat(PostgresqlUtils.toQueryValue(123.1)).isEqualTo("123.1");
        assertThat(PostgresqlUtils.toQueryValue("foobar")).isEqualTo("foobar");
        assertThat(PostgresqlUtils.toQueryValue("1.0")).isEqualTo("1.0");
        assertThat(PostgresqlUtils.toQueryValue(new LinkedHashMap<>(Collections.singletonMap("foo", "bar")))).isEqualTo("{\"foo\":\"bar\"}");
        assertThat(PostgresqlUtils.toQueryValue(List.of("foo", "bar"))).isEqualTo("[\"foo\",\"bar\"]");
        assertThat(PostgresqlUtils.toQueryValue(new ObjectId("foobarfoobar".getBytes(StandardCharsets.UTF_8)))).isEqualTo("{\"@class\":\"de.bwaldvogel.mongo.bson.ObjectId\",\"data\":\"Zm9vYmFyZm9vYmFy\"}");
        assertThat(PostgresqlUtils.toQueryValue(new Document("key", "value"))).isEqualTo("{\"@class\":\"de.bwaldvogel.mongo.bson.Document\",\"key\":\"value\"}");

        assertThatExceptionOfType(NullPointerException.class)
            .isThrownBy(() -> PostgresqlUtils.toQueryValue(null))
            .withMessage(null);
    }

}
<file_sep>/test-common/src/main/java/de/bwaldvogel/AbstractReadOnlyProxyTest.java
package de.bwaldvogel;

import static de.bwaldvogel.mongo.backend.TestUtils.json;
import static de.bwaldvogel.mongo.backend.TestUtils.jsonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.util.List;

import org.bson.Document;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.util.CollectionUtils;

import com.mongodb.MongoCommandException;
import com.mongodb.MongoException;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.model.Updates;
import com.mongodb.connection.ServerDescription;

import de.bwaldvogel.mongo.MongoBackend;
import de.bwaldvogel.mongo.MongoServer;
import de.bwaldvogel.mongo.backend.ReadOnlyProxy;

public abstract class AbstractReadOnlyProxyTest {

    private MongoClient readOnlyClient;
    private MongoServer mongoServer;
    private MongoServer writeableServer;
    private MongoClient writeClient;

    protected abstract MongoBackend createBackend() throws Exception;

    @BeforeEach
    public void setUp() throws Exception {
        MongoBackend mongoBackend = createBackend();
        writeableServer = new MongoServer(mongoBackend);
        writeClient = MongoClients.create(writeableServer.bindAndGetConnectionString());

        mongoServer = new MongoServer(new ReadOnlyProxy(mongoBackend));
        readOnlyClient = MongoClients.create(mongoServer.bindAndGetConnectionString());
    }

    @AfterEach
    public void tearDown() {
        writeClient.close();
        readOnlyClient.close();
        mongoServer.shutdownNow();
        writeableServer.shutdownNow();
    }

    @Test
    void testMaxDocumentSize() throws Exception {
        ServerDescription serverDescription = CollectionUtils.getOnlyElement(readOnlyClient.getClusterDescription().getServerDescriptions());
        assertThat(serverDescription.getMaxDocumentSize()).isEqualTo(16777216);
    }

    @Test
    void testServerStatus() throws Exception {
        readOnlyClient.getDatabase("admin").runCommand(new Document("serverStatus", 1));
    }

    @Test
    void testCurrentOperations() throws Exception {
        Document currentOperations = readOnlyClient.getDatabase("admin").getCollection("$cmd.sys.inprog").find().first();
        assertThat(currentOperations).isNotNull();
    }

    @Test
    void testStats() throws Exception {
        Document stats = readOnlyClient.getDatabase("testdb").runCommand(json("dbStats:1"));
        assertThat(((Number) stats.get("objects")).longValue()).isZero();
    }

    @Test
    void testListDatabaseNames() throws Exception {
        assertThat(readOnlyClient.listDatabaseNames()).isEmpty();
        writeClient.getDatabase("testdb").getCollection("testcollection").insertOne(new Document());
        assertThat(readOnlyClient.listDatabaseNames()).containsExactly("testdb");
        writeClient.getDatabase("bar").getCollection("testcollection").insertOne(new Document());
        assertThat(readOnlyClient.listDatabaseNames()).containsExactly("bar", "testdb");
    }

    @Test
    void testIllegalCommand() throws Exception {
        assertThatExceptionOfType(MongoException.class)
            .isThrownBy(() -> readOnlyClient.getDatabase("testdb").runCommand(json("foo:1")))
            .withMessageContaining("Command failed with error 59 (CommandNotFound): 'no such command: 'foo'");

        assertThatExceptionOfType(MongoException.class)
            .isThrownBy(() -> readOnlyClient.getDatabase("bar").runCommand(json("foo:1")))
            .withMessageContaining("Command failed with error 59 (CommandNotFound): 'no such command: 'foo'");
    }

    @Test
    void testQuery() throws Exception {
        MongoCollection<Document> collection = readOnlyClient.getDatabase("testdb").getCollection("testcollection");
        Document obj = collection.find(json("_id: 1")).first();
        assertThat(obj).isNull();
        assertThat(collection.countDocuments()).isEqualTo(0);
    }

    @Test
    void testDistinctQuery() {
        MongoCollection<Document> collection = writeClient.getDatabase("testdb").getCollection("testcollection");
        collection.insertOne(new Document("n", 1));
        collection.insertOne(new Document("n", 2));
        collection.insertOne(new Document("n", 1));
        collection = readOnlyClient.getDatabase("testdb").getCollection("testcollection");
        assertThat(collection.distinct("n", Integer.class)).containsExactly(1, 2);
    }

    @Test
    void testInsert() throws Exception {
        MongoCollection<Document> collection = readOnlyClient.getDatabase("testdb").getCollection("testcollection");
        assertThat(collection.countDocuments()).isZero();

        assertThatExceptionOfType(MongoException.class)
            .isThrownBy(() -> collection.insertOne(json("{}")));
    }

    @Test
    void testUpdate() throws Exception {
        MongoCollection<Document> collection = readOnlyClient.getDatabase("testdb").getCollection("testcollection");
        Document object = new Document("_id", 1);
        Document newObject = new Document("_id", 1);

        assertThatExceptionOfType(MongoException.class)
            .isThrownBy(() -> collection.replaceOne(object, newObject))
            .withMessageContaining("Command failed with error 59 (CommandNotFound): 'no such command: 'update'");
    }

    @Test
    void testUpsert() throws Exception {
        MongoCollection<Document> collection = readOnlyClient.getDatabase("testdb").getCollection("testcollection");

        assertThatExceptionOfType(MongoException.class)
            .isThrownBy(() -> collection.updateMany(json("{}"), Updates.set("foo", "bar"), new UpdateOptions().upsert(true)))
            .withMessageContaining("Command failed with error 59 (CommandNotFound): 'no such command: 'update'");
    }

    @Test
    void testDropDatabase() throws Exception {
        MongoDatabase database = readOnlyClient.getDatabase("testdb");

        assertThatExceptionOfType(MongoException.class)
            .isThrownBy(database::drop);
    }

    @Test
    void testDropCollection() throws Exception {
        MongoCollection<Document> collection = readOnlyClient.getDatabase("testdb").getCollection("foo");

        assertThatExceptionOfType(MongoException.class)
            .isThrownBy(collection::drop)
            .withMessageContaining("Command failed with error 59 (CommandNotFound): 'no such command: 'drop'");
    }

    @Test
    void testHandleKillCursor() {
        MongoCollection<Document> collection = writeClient.getDatabase("testdb").getCollection("testcollection");
        collection.insertMany(List.of(new Document(), new Document()));
        MongoCursor<Document> cursor = readOnlyClient.getDatabase("testdb")
            .getCollection("testcollection").find().batchSize(1).cursor();
        assertThat(cursor.getServerCursor()).isNotNull();
        while (cursor.hasNext()) {
            cursor.next();
        }
        assertThat(cursor.getServerCursor()).isNull();
    }

    @Test
    void testAggregateWithExpressionProjection() throws Exception {
        List<Document> pipeline = jsonList("$project: {_id: 0, idHex: {$toString: '$_id'}}");

        MongoCollection<Document> collection = readOnlyClient.getDatabase("testdb").getCollection("testcollection");
        assertThat(collection.aggregate(pipeline)).isEmpty();
    }

    @Test
    void testAggregateWithOut() {
        List<Document> pipeline = jsonList(
            "$group: {_id: '$author', books: {$push: '$title'}}",
            "$out : 'authors'");

        MongoCollection<Document> collection = readOnlyClient.getDatabase("testdb").getCollection("testcollection");

        assertThatExceptionOfType(MongoCommandException.class)
            .isThrownBy(() -> collection.aggregate(pipeline).first())
            .withMessageContaining("Command failed with error -1: 'Aggregation contains a modifying stage and is therefore not allowed in read-only mode'");
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/IllegalOperationException.java
package de.bwaldvogel.mongo.exception;

public class IllegalOperationException extends MongoServerError {

    private static final long serialVersionUID = 1L;

    public IllegalOperationException(String message) {
        super(ErrorCode.IllegalOperation, message);
    }

}
<file_sep>/postgresql-backend/build.gradle
dependencies {
    api project(':mongo-java-server-core')

    implementation "com.fasterxml.jackson.core:jackson-core:latest.release"
    implementation "com.fasterxml.jackson.core:jackson-databind:latest.release"
    implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:latest.release"

    testRuntimeOnly "org.postgresql:postgresql:latest.release"
    testImplementation "com.zaxxer:HikariCP:[4.0.0, 5.0.0)"
    testImplementation 'org.testcontainers:testcontainers:latest.release'
    testImplementation project(':mongo-java-server-test-common')
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/stage/SampleStage.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import java.security.SecureRandom;
import java.util.Comparator;
import java.util.stream.Stream;

import de.bwaldvogel.mongo.backend.Missing;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.MongoServerError;

public class SampleStage implements AggregationStage {

    private static final SecureRandom random = new SecureRandom();

    private final int size;

    public SampleStage(Object sample) {
        if (!(sample instanceof Document)) {
            throw new MongoServerError(28745, "the $sample stage specification must be an object");
        }

        Document size = (Document) sample;
        Object sizeValue = size.getOrMissing("size");

        if (sizeValue instanceof Missing) {
            throw new MongoServerError(28749, "$sample stage must specify a size");
        }

        if (!(sizeValue instanceof Number)) {
            throw new MongoServerError(28746, "size argument to $sample must be a number");
        }

        this.size = ((Number) sizeValue).intValue();

        if (this.size < 0) {
            throw new MongoServerError(28747, "size argument to $sample must not be negative");
        }

        for (String key : size.keySet()) {
            if (!key.equals("size")) {
                throw new MongoServerError(28748, "unrecognized option to $sample: " + key);
            }
        }
    }

    @Override
    public String name() {
        return "$sample";
    }

    @Override
    public Stream<Document> apply(Stream<Document> stream) {
        return stream
            .sorted(Comparator.comparingDouble(key -> random.nextDouble()))
            .limit(size);
    }
}
<file_sep>/postgresql-backend/src/main/java/de/bwaldvogel/mongo/backend/postgresql/PostgresqlBackend.java
package de.bwaldvogel.mongo.backend.postgresql;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.Clock;

import javax.sql.DataSource;

import de.bwaldvogel.mongo.MongoDatabase;
import de.bwaldvogel.mongo.backend.AbstractMongoBackend;
import de.bwaldvogel.mongo.exception.MongoServerException;

public class PostgresqlBackend extends AbstractMongoBackend {

    private final DataSource dataSource;

    public PostgresqlBackend(DataSource dataSource) {
        this(dataSource, defaultClock());
    }

    public PostgresqlBackend(DataSource dataSource, Clock clock) {
        super(clock);
        this.dataSource = dataSource;
    }

    @Override
    protected MongoDatabase openOrCreateDatabase(String databaseName) {
        String sql = "CREATE TABLE IF NOT EXISTS " + databaseName + "._meta" +
            " (collection_name text," +
            "  datasize bigint," +
            " CONSTRAINT pk_meta PRIMARY KEY (collection_name)" +
            ")";
        try (Connection connection = getConnection();
             PreparedStatement stmt1 = connection.prepareStatement("CREATE SCHEMA IF NOT EXISTS " + databaseName);
             PreparedStatement stmt2 = connection.prepareStatement(sql)
        ) {
            stmt1.executeUpdate();
            stmt2.executeUpdate();
        } catch (SQLException e) {
            throw new MongoServerException("failed to open or create database", e);
        }

        return new PostgresqlDatabase(databaseName, this, getCursorRegistry());
    }

    public Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/bson/MinKey.java
package de.bwaldvogel.mongo.bson;

public class MinKey implements Bson {

    private static final long serialVersionUID = 1L;

    private static final MinKey INSTANCE = new MinKey();

    private MinKey() {
    }

    public static MinKey getInstance() {
        return INSTANCE;
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/wire/message/MongoQuery.java
package de.bwaldvogel.mongo.wire.message;

import de.bwaldvogel.mongo.bson.Document;
import io.netty.channel.Channel;

public class MongoQuery extends ClientRequest {

    private final Document query;
    private final Document returnFieldSelector;
    private final int numberToSkip;
    private final int numberToReturn;

    public MongoQuery(Channel channel, MessageHeader header, String fullCollectionName, int numberToSkip,
            int numberToReturn, Document query, Document returnFieldSelector) {
        super(channel, header, fullCollectionName);
        this.numberToSkip = numberToSkip;
        this.numberToReturn = numberToReturn;
        this.query = query;
        this.returnFieldSelector = returnFieldSelector;
    }

    public int getNumberToSkip() {
        return numberToSkip;
    }

    public int getNumberToReturn() {
        return numberToReturn;
    }

    public Document getQuery() {
        return query;
    }

    public Document getReturnFieldSelector() {
        return returnFieldSelector;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getSimpleName()).append("(");
        sb.append("header: ").append(getHeader());
        sb.append(", collection: ").append(getFullCollectionName());
        sb.append(", query: ").append(query);
        sb.append(", returnFieldSelector: ").append(returnFieldSelector);
        sb.append(")");
        return sb.toString();
    }

}
<file_sep>/test-common/build.gradle
dependencies {
    api project(':mongo-java-server-core')
    api 'org.mongodb:mongodb-driver-sync:latest.release'
    api 'org.mongodb:mongodb-driver-reactivestreams:latest.release'

    api "org.springframework:spring-core:latest.release"
    api "org.springframework:spring-beans:latest.release"
    api "org.springframework:spring-context:latest.release"
    api "org.springframework:spring-test:latest.release"
    api "org.springframework.data:spring-data-mongodb:latest.release"

    api group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: 'latest.release'
    api group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: 'latest.release'
    runtimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: 'latest.release'
    api group: 'org.assertj', name: 'assertj-core', version: 'latest.release'
    api "org.mockito:mockito-core:latest.release"
    api "org.mockito:mockito-junit-jupiter:latest.release"

    runtimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: '[1.3.0, 1.4.0)'

    testImplementation "org.testcontainers:testcontainers:latest.release"
}
<file_sep>/CONTRIBUTING.md
## Contributing ##

We are happy to receive pull-requests if you want to contribute new features or bugfixes to the project.
Please use the code style settings that you can find in the [code-style directory](code-style/).
If you use IntelliJ, please make sure it properly picks up the settings from the [EditorConfig file](.editorconfig)
(this should be the default behavior though).

Issues labeled [help wanted][issues-help-wanted] or [good first issue][issues-good-first-issue]
can be good first contributions.

If you want to thank the author for this library or want to support the maintenance work,
we are happy to receive a donation.

[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/BenediktWaldvogel)

[issues-help-wanted]: https://github.com/bwaldvogel/mongo-java-server/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22
[issues-good-first-issue]: https://github.com/bwaldvogel/mongo-java-server/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22
<file_sep>/memory-backend/src/test/java/de/bwaldvogel/mongo/MemoryBackendReadOnlyProxyTest.java
package de.bwaldvogel.mongo;

import de.bwaldvogel.AbstractReadOnlyProxyTest;
import de.bwaldvogel.mongo.backend.memory.MemoryBackend;

class MemoryBackendReadOnlyProxyTest extends AbstractReadOnlyProxyTest {

    @Override
    protected MongoBackend createBackend() {
        return new MemoryBackend();
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/ConflictingUpdateOperatorsException.java
package de.bwaldvogel.mongo.exception;

public class ConflictingUpdateOperatorsException extends MongoServerError {

    private static final long serialVersionUID = 1L;

    public ConflictingUpdateOperatorsException(String updatePath, String conflictingPath) {
        super(ErrorCode.ConflictingUpdateOperators,
            "Updating the path '" + updatePath + "' would create a conflict at '" + conflictingPath + "'");
    }

    @Override
    public boolean shouldPrefixCommandContext() {
        return false;
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/wire/message/MessageHeader.java
package de.bwaldvogel.mongo.wire.message;

public class MessageHeader {

    private final int totalLength;
    private final int requestID;
    private final int responseTo;

    public MessageHeader(int requestID, int responseTo) {
        this(0, requestID, responseTo);
    }

    public MessageHeader(int totalLength, int requestID, int responseTo) {
        this.totalLength = totalLength;
        this.requestID = requestID;
        this.responseTo = responseTo;
    }

    public int getTotalLength() {
        return totalLength;
    }

    public int getRequestID() {
        return requestID;
    }

    public int getResponseTo() {
        return responseTo;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getSimpleName());
        sb.append("(");
        sb.append("request: ").append(requestID);
        sb.append(", responseTo: ").append(responseTo);
        if (totalLength > 0) {
            sb.append(", length: ").append(totalLength);
        }
        sb.append(")");
        return sb.toString();
    }

}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/AssertTest.java
package de.bwaldvogel.mongo.backend;

import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.junit.jupiter.api.Test;

public class AssertTest {

    @Test
    void testIsEmpty() throws Exception {
        Assert.isEmpty(Collections.emptyList());
        Assert.isEmpty(Collections.emptySet());
        Assert.isEmpty(new ArrayList<>());

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.isEmpty(List.of("a", "b", "c")))
            .withMessage("Expected [a, b, c] to be empty");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.isEmpty(List.of("a", "b", "c"), () -> "some message"))
            .withMessage("some message");
    }

    @Test
    void testNotEmpty() throws Exception {
        Assert.notEmpty(List.of("a", "b", "c"));

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.notEmpty(Collections.emptySet()))
            .withMessage("Given collection must not be empty");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.notEmpty(Collections.emptySet(), () -> "some message"))
            .withMessage("some message");
    }

    @Test
    void testHasSize() throws Exception {
        Assert.hasSize(List.of("a", "b", "c"), 3);

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.hasSize(Collections.emptySet(), 1))
            .withMessage("Expected [] to have size 1 but got 0 elements");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.hasSize(Collections.emptySet(), 1, () -> "some message"))
            .withMessage("some message");
    }

    @Test
    void testEquals() throws Exception {
        Assert.equals(null, null);
        Assert.equals("a", "a");
        Assert.equals(123, 123);

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.equals("a", "b"))
            .withMessage("Expected 'a' to be equal to 'b'");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.equals(1, 2))
            .withMessage("Expected 1 to be equal to 2");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.equals("a", "b", () -> "some message"))
            .withMessage("some message");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.equals(1, 2, () -> "some message"))
            .withMessage("some message");
    }

    @Test
    void testIsTrue() throws Exception {
        Assert.isTrue(true, () -> "");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.isTrue(false, () -> "some message"))
            .withMessage("some message");
    }

    @Test
    void testIsFalse() throws Exception {
        Assert.isFalse(false, () -> "");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.isFalse(true, () -> "some message"))
            .withMessage("some message");
    }

    @Test
    void testIsNull() throws Exception {
        Assert.isNull(null);

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.isNull("abc"))
            .withMessage("Given value 'abc' must not be null");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.isNull(new Object(), () -> "some message"))
            .withMessage("some message");
    }

    @Test
    void testNotNull() throws Exception {
        Assert.notNull("");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.notNull(null))
            .withMessage("Given value must not be null");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.notNull(null, () -> "some message"))
            .withMessage("some message");
    }

    @Test
    void testStartsWith() throws Exception {
        Assert.startsWith("abc", "ab");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.startsWith("abc", "b"))
            .withMessage("'abc' must start with 'b'");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.startsWith("abc", "b", () -> "some message"))
            .withMessage("some message");
    }

    @Test
    void testDoesNotStartWith() throws Exception {
        Assert.doesNotStartWith("a", "b");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.doesNotStartWith("abc", "ab"))
            .withMessage("'abc' must not start with 'ab'");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.doesNotStartWith("abc", "ab", () -> "some message"))
            .withMessage("some message");
    }

    @Test
    void testNotNullOrEmpty() throws Exception {
        Assert.notNullOrEmpty("a");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.notNullOrEmpty(null))
            .withMessage("Given string 'null' must not be null or empty");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.notNullOrEmpty(""))
            .withMessage("Given string '' must not be null or empty");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(() -> Assert.notNullOrEmpty("", () -> "some message"))
            .withMessage("some message");
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/wire/MessageFlag.java
package de.bwaldvogel.mongo.wire;

public enum MessageFlag {
    CHECKSUM_PRESENT(0),
    MORE_TO_COME(1),
    EXHAUST_ALLOWED(16),
    ;

    private final int value;

    MessageFlag(int bit) {
        this.value = 1 << bit;
    }

    public boolean isSet(int flags) {
        return (flags & value) == value;
    }

    public int removeFrom(int flags) {
        return flags - value;
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/oplog/CollectionBackedOplog.java
package de.bwaldvogel.mongo.oplog;

import java.util.List;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Stream;

import de.bwaldvogel.mongo.MongoBackend;
import de.bwaldvogel.mongo.MongoCollection;
import de.bwaldvogel.mongo.backend.Cursor;
import de.bwaldvogel.mongo.backend.CursorRegistry;
import de.bwaldvogel.mongo.backend.Utils;
import de.bwaldvogel.mongo.backend.aggregation.Aggregation;
import de.bwaldvogel.mongo.bson.BsonTimestamp;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.MongoServerException;

public class CollectionBackedOplog implements Oplog {

    private static final long ELECTION_TERM = 1L;
    private static final String START_AT_OPERATION_TIME = "startAtOperationTime";
    private static final String FULL_DOCUMENT = "fullDocument";
    private static final String START_AFTER = "startAfter";
    private static final String RESUME_AFTER = "resumeAfter";
    private static final String OPERATION_TYPE = "operationType";
    private static final String CLUSTER_TIME = "clusterTime";
    private static final String DOCUMENT_KEY = "documentKey";

    private final OplogClock oplogClock;
    private final MongoCollection<Document> collection;
    private final MongoBackend backend;
    private final CursorRegistry cursorRegistry;
    private final UUID ui = UUID.randomUUID();

    public CollectionBackedOplog(MongoBackend backend, MongoCollection<Document> collection, CursorRegistry cursorRegistry) {
        this.oplogClock = new OplogClock(backend.getClock());
        this.collection = collection;
        this.backend = backend;
        this.cursorRegistry = cursorRegistry;
    }

    @Override
    public void handleInsert(String namespace, List<Document> documents) {
        if (isOplogCollection(namespace)) {
            return;
        }
        Stream<Document> oplogInsertDocuments = documents.stream()
            .map(document -> toOplogInsertDocument(namespace, document));
        addDocuments(oplogInsertDocuments);
    }

    @Override
    public void handleUpdate(String namespace, Document selector, Document query, List<Object> modifiedIds) {
        if (isOplogCollection(namespace)) {
            return;
        }
        Stream<Document> oplogUpdateDocuments = modifiedIds.stream()
            .map(id -> toOplogUpdateDocument(namespace, query, id));
        addDocuments(oplogUpdateDocuments);
    }

    @Override
    public void handleDelete(String namespace, Document query, List<Object> deletedIds) {
        if (isOplogCollection(namespace)) {
            return;
        }
        Stream<Document> oplogDeleteDocuments = deletedIds.stream()
            .map(id -> toOplogDeleteDocument(namespace, id));
        addDocuments(oplogDeleteDocuments);
    }

    private void addDocuments(Stream<Document> oplogDocuments) {
        collection.addDocuments(oplogDocuments);
    }

    @Override
    public void handleDropCollection(String namespace) {
        if (isOplogCollection(namespace)) {
            return;
        }
        final String databaseName = Utils.getDatabaseNameFromFullName(namespace);
        final String collectionName = Utils.getCollectionNameFromFullName(namespace);
        collection.addDocument(toOplogDropCollection(databaseName, collectionName));
    }

    private Stream<Document> streamOplog(Document changeStreamDocument, OplogPosition position, Aggregation aggregation,
                                         String namespace) {
        return aggregation.runStagesAsStream(collection.queryAllAsStream()
            .filter(document -> filterNamespace(document, namespace))
            .filter(document -> {
                BsonTimestamp timestamp = getOplogTimestamp(document);
                OplogPosition documentOplogPosition = new OplogPosition(timestamp);
                return documentOplogPosition.isAfter(position);
            })
            .sorted((o1, o2) -> {
                BsonTimestamp timestamp1 = getOplogTimestamp(o1);
                BsonTimestamp timestamp2 = getOplogTimestamp(o2);
                return timestamp1.compareTo(timestamp2);
            })
            .map(document -> toChangeStreamResponseDocument(document, changeStreamDocument)));
    }

    private static boolean filterNamespace(Document document, String namespace) {
        String docNS = (String) document.get(OplogDocumentFields.NAMESPACE);
        if (docNS.equals(namespace)) {
            return true;
        }
        return Utils.getDatabaseNameFromFullName(namespace).equals(Utils.getDatabaseNameFromFullName(docNS))
            && Utils.getCollectionNameFromFullName(docNS).equals("$cmd");
    }

    @Override
    public Cursor createCursor(Document changeStreamDocument, String namespace, Aggregation aggregation) {
        Document startAfter = (Document) changeStreamDocument.get(START_AFTER);
        Document resumeAfter = (Document) changeStreamDocument.get(RESUME_AFTER);
        BsonTimestamp startAtOperationTime = (BsonTimestamp) changeStreamDocument.get(START_AT_OPERATION_TIME);
        final OplogPosition initialOplogPosition;
        if (startAfter != null) {
            initialOplogPosition = OplogPosition.fromDocument(startAfter);
        } else if (resumeAfter != null) {
            initialOplogPosition = OplogPosition.fromDocument(resumeAfter);
            String databaseName = Utils.getDatabaseNameFromFullName(namespace);
            String collectionName = Utils.getCollectionNameFromFullName(namespace);
            boolean resumeAfterTerminalEvent = collection.queryAllAsStream()
                .filter(document -> {
                    BsonTimestamp timestamp = getOplogTimestamp(document);
                    OplogPosition documentOplogPosition = new OplogPosition(timestamp);
                    return initialOplogPosition.isAfter(documentOplogPosition.inclusive());
                })
                .anyMatch(document -> document.get(OplogDocumentFields.OPERATION_TYPE).equals(OperationType.COMMAND.getCode())
                    && document.get(OplogDocumentFields.NAMESPACE).equals(String.format("%s.$cmd", databaseName))
                    && document.get(OplogDocumentFields.O).equals(new Document("drop", collectionName))
                );

            if (resumeAfterTerminalEvent) {
                return new InvalidateOplogCursor(initialOplogPosition);
            }
        } else if (startAtOperationTime != null) {
            initialOplogPosition = new OplogPosition(startAtOperationTime).inclusive();
        } else {
            initialOplogPosition = new OplogPosition(oplogClock.now());
        }

        Function<OplogPosition, Stream<Document>> streamSupplier =
            position -> streamOplog(changeStreamDocument, position, aggregation, namespace);
        OplogCursor cursor = new OplogCursor(cursorRegistry.generateCursorId(), streamSupplier, initialOplogPosition);
        cursorRegistry.add(cursor);
        return cursor;
    }

    private Document toOplogDocument(OperationType operationType, String namespace) {
        return new Document()
            .append(OplogDocumentFields.TIMESTAMP, oplogClock.incrementAndGet())
            .append("t", ELECTION_TERM)
            .append("h", 0L)
            .append("v", 2L)
            .append("op", operationType.getCode())
            .append(OplogDocumentFields.NAMESPACE, namespace)
            .append("ui", ui)
            .append("wall", oplogClock.instant());
    }

    private Document toOplogInsertDocument(String namespace, Document document) {
        return toOplogDocument(OperationType.INSERT, namespace)
            .append(OplogDocumentFields.O, document.cloneDeeply());
    }

    private Document toOplogUpdateDocument(String namespace, Document query, Object id) {
        return toOplogDocument(OperationType.UPDATE, namespace)
            .append(OplogDocumentFields.O, query)
            .append(OplogDocumentFields.O2, new Document(OplogDocumentFields.ID, id));
    }

    private Document toOplogDeleteDocument(String namespace, Object deletedDocumentId) {
        return toOplogDocument(OperationType.DELETE, namespace)
            .append(OplogDocumentFields.O, new Document(OplogDocumentFields.ID, deletedDocumentId));
    }

    private Document toOplogDropCollection(String databaseName, String collectionName) {
        return toOplogDocument(OperationType.COMMAND, String.format("%s.$cmd", databaseName))
            .append(OplogDocumentFields.O, new Document("drop", collectionName));
    }

    private boolean isOplogCollection(String namespace) {
        return collection.getFullName().equals(namespace);
    }

    private Document getFullDocument(Document changeStreamDocument, Document document, OperationType operationType) {
        switch (operationType) {
            case INSERT:
                return getUpdateDocument(document);
            case DELETE:
                return null;
            case UPDATE:
                return lookUpUpdateDocument(changeStreamDocument, document);
        }
        throw new IllegalArgumentException("Invalid operation type");
    }

    private Document lookUpUpdateDocument(Document changeStreamDocument, Document document) {
        Document deltaUpdate = getDeltaUpdate(getUpdateDocument(document));
        if (changeStreamDocument.containsKey(FULL_DOCUMENT) && changeStreamDocument.get(FULL_DOCUMENT).equals("updateLookup")) {
            String namespace = (String) document.get(OplogDocumentFields.NAMESPACE);
            String databaseName = namespace.split("\\.")[0];
            String collectionName = namespace.split("\\.")[1];
            return backend.resolveDatabase(databaseName)
                .resolveCollection(collectionName, true)
                .queryAllAsStream()
                .filter(d -> d.get(OplogDocumentFields.ID).equals(((Document) document.get(OplogDocumentFields.O2)).get(OplogDocumentFields.ID)))
                .findFirst()
                .orElse(deltaUpdate);
        }
        return deltaUpdate;
    }

    private Document getDeltaUpdate(Document updateDocument) {
        Document delta = new Document();
        if (updateDocument.containsKey("$set")) {
            delta.appendAll((Document) updateDocument.get("$set"));
        }
        if (updateDocument.containsKey("$unset")) {
            delta.appendAll((Document) updateDocument.get("$unset"));
        }
        return delta;
    }

    private Document toChangeStreamResponseDocument(Document oplogDocument, Document changeStreamDocument) {
        OperationType operationType = OperationType.fromCode(oplogDocument.get(OplogDocumentFields.OPERATION_TYPE).toString());
        Document documentKey = new Document();
        Document document = getUpdateDocument(oplogDocument);
        BsonTimestamp timestamp = getOplogTimestamp(oplogDocument);
        OplogPosition oplogPosition = new OplogPosition(timestamp);
        switch (operationType) {
            case UPDATE:
            case DELETE:
                documentKey = document;
                break;
            case INSERT:
                documentKey.append(OplogDocumentFields.ID, document.get(OplogDocumentFields.ID));
                break;
            case COMMAND:
                return toChangeStreamCommandResponseDocument(oplogDocument, oplogPosition, timestamp);
            default:
                throw new IllegalArgumentException("Unexpected operation type: " + operationType);
        }

        return new Document()
            .append(OplogDocumentFields.ID, new Document(OplogDocumentFields.ID_DATA_KEY, oplogPosition.toHexString()))
            .append(OPERATION_TYPE, operationType.getDescription())
            .append(FULL_DOCUMENT, getFullDocument(changeStreamDocument, oplogDocument, operationType))
            .append(DOCUMENT_KEY, documentKey)
            .append(CLUSTER_TIME, timestamp);
    }

    private Document toChangeStreamCommandResponseDocument(Document oplogDocument, OplogPosition oplogPosition, BsonTimestamp timestamp) {
        Document document = getUpdateDocument(oplogDocument);
        String operationType = document.keySet().stream().findFirst().orElseThrow(
            () -> new MongoServerException("Unspecified command operation type")
        );

        return new Document()
            .append(OplogDocumentFields.ID, new Document(OplogDocumentFields.ID_DATA_KEY, oplogPosition.toHexString()))
            .append(OPERATION_TYPE, operationType)
            .append(CLUSTER_TIME, timestamp);
    }

    private static BsonTimestamp getOplogTimestamp(Document document) {
        return (BsonTimestamp) document.get(OplogDocumentFields.TIMESTAMP);
    }

    private static Document getUpdateDocument(Document document) {
        return (Document) document.get(OplogDocumentFields.O);
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/QueryResult.java
package de.bwaldvogel.mongo.backend;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import de.bwaldvogel.mongo.bson.Document;

public class QueryResult implements Iterable<Document> {

    private final Iterable<Document> documents;
    private final long cursorId;

    public QueryResult() {
        this(Collections.emptyList(), EmptyCursor.get());
    }

    public QueryResult(Iterable<Document> documents, Cursor cursor) {
        this(documents, cursor.getId());
    }

    public QueryResult(Iterable<Document> documents, long cursorId) {
        this.documents = documents;
        this.cursorId = cursorId;
    }

    public QueryResult(Iterable<Document> documents) {
        this(documents, EmptyCursor.get());
    }

    public List<Document> collectDocuments() {
        if (documents instanceof List<?>) {
            return Collections.unmodifiableList((List<Document>) documents);
        } else {
            List<Document> documents = new ArrayList<>();
            for (Document document : this) {
                documents.add(document);
            }
            return documents;
        }
    }

    @Override
    public Iterator<Document> iterator() {
        return documents.iterator();
    }

    public long getCursorId() {
        return cursorId;
    }
}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/AbstractMongoCollectionTest.java
package de.bwaldvogel.mongo.backend;

import static de.bwaldvogel.mongo.TestUtils.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.util.stream.Stream;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import de.bwaldvogel.mongo.MongoDatabase;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.bson.ObjectId;
import de.bwaldvogel.mongo.exception.ConflictingUpdateOperatorsException;

public class AbstractMongoCollectionTest {

    private static class TestCollection extends AbstractMongoCollection<Object> {

        TestCollection(MongoDatabase database, String collectionName) {
            super(database, collectionName, CollectionOptions.withDefaults(), new CursorRegistry());
        }

        @Override
        protected Object addDocumentInternal(Document document) {
            throw new UnsupportedOperationException();
        }

        @Override
        public int count() {
            throw new UnsupportedOperationException();
        }

        @Override
        protected Document getDocument(Object position) {
            throw new UnsupportedOperationException();
        }

        @Override
        protected void removeDocument(Object position) {
            throw new UnsupportedOperationException();
        }

        @Override
        protected QueryResult matchDocuments(Document query, Document orderBy, int numberToSkip,
                                             int numberToReturn, int batchSize, Document fieldSelector) {
            throw new UnsupportedOperationException();
        }

        @Override
        protected void updateDataSize(int sizeDelta) {
        }

        @Override
        protected int getDataSize() {
            throw new UnsupportedOperationException();
        }

        @Override
        protected void handleUpdate(Object position, Document oldDocument, Document newDocument) {
            // noop
        }

        @Override
        protected Stream<DocumentWithPosition<Object>> streamAllDocumentsWithPosition() {
            throw new UnsupportedOperationException();
        }

    }

    private TestCollection collection;

    @BeforeEach
    void setUp() {
        MongoDatabase database = Mockito.mock(MongoDatabase.class);
        this.collection = new TestCollection(database, "some collection");
    }

    @Test
    void testConvertSelector() throws Exception {
        assertThat(collection.convertSelectorToDocument(json("")))
            .isEqualTo(json(""));

        assertThat(collection.convertSelectorToDocument(json("_id: 1")))
            .isEqualTo(json("_id: 1"));

        assertThat(collection.convertSelectorToDocument(json("_id: 1, $set: {foo: 'bar'}")))
            .isEqualTo(json("_id: 1"));

        assertThat(collection.convertSelectorToDocument(json("_id: 1, 'e.i': 14")))
            .isEqualTo(json("_id: 1, e: {i: 14}"));

        assertThat(collection.convertSelectorToDocument(json("_id: 1, 'e.i.y': {foo: 'bar'}")))
            .isEqualTo(json("_id: 1, e: {i: {y: {foo: 'bar'}}}"));
    }

    @Test
    void testDeriveDocumentId() throws Exception {
        assertThat(collection.deriveDocumentId(json(""))).isInstanceOf(ObjectId.class);
        assertThat(collection.deriveDocumentId(json("a: 1"))).isInstanceOf(ObjectId.class);
        assertThat(collection.deriveDocumentId(json("_id: 1"))).isEqualTo(1);
        assertThat(collection.deriveDocumentId(json("_id: {$in: [1]}"))).isEqualTo(1);
        assertThat(collection.deriveDocumentId(json("_id: {$in: []}"))).isInstanceOf(ObjectId.class);
    }

    @Test
    void testValidateUpdateQuery() throws Exception {
        AbstractMongoCollection.validateUpdateQuery(new Document());
        AbstractMongoCollection.validateUpdateQuery(json("$set: {a: 1, b: 1}"));
        AbstractMongoCollection.validateUpdateQuery(json("$set: {a: 1, b: 1}, $inc: {c: 1}"));
        AbstractMongoCollection.validateUpdateQuery(json("$set: {'a.b.c': 1}, $inc: {'a.b.d': 1}"));
        AbstractMongoCollection.validateUpdateQuery(json("$set: {'a.b.c': 1}, $inc: {'a.c': 1}"));
        AbstractMongoCollection.validateUpdateQuery(json("$inc: {'a.$.y': 1, 'a.$.x': 1}"));
        AbstractMongoCollection.validateUpdateQuery(json("$inc: {'a.$.deleted': 1, 'a.$.deletedBy': 1}"));

        assertThatExceptionOfType(ConflictingUpdateOperatorsException.class)
            .isThrownBy(() -> AbstractMongoCollection.validateUpdateQuery(json("$set: {a: 1, b: 1}, $inc: {b: 1}")))
            .withMessage("[Error 40] Updating the path 'b' would create a conflict at 'b'");

        assertThatExceptionOfType(ConflictingUpdateOperatorsException.class)
            .isThrownBy(() -> AbstractMongoCollection.validateUpdateQuery(json("$set: {'a.b.c': 1}, $inc: {'a.b': 1}")))
            .withMessage("[Error 40] Updating the path 'a.b' would create a conflict at 'a.b'");
    }

}
<file_sep>/memory-backend/src/main/java/de/bwaldvogel/mongo/backend/memory/DocumentIterable.java
package de.bwaldvogel.mongo.backend.memory;

import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

import de.bwaldvogel.mongo.bson.Document;

class DocumentIterable implements Iterable<Document> {

    private final List<Document> documents;
    private final boolean reversed;

    DocumentIterable(List<Document> documents) {
        this(documents, false);
    }

    private DocumentIterable(List<Document> documents, boolean reversed) {
        this.documents = documents;
        this.reversed = reversed;
    }

    DocumentIterable reversed() {
        return new DocumentIterable(documents, !reversed);
    }

    @Override
    public Iterator<Document> iterator() {
        if (!reversed) {
            return new DocumentIterator(documents);
        } else {
            return new ReverseDocumentIterator(documents);
        }
    }

    private abstract static class AbstractDocumentIterator implements Iterator<Document> {

        int pos;
        final List<Document> documents;
        Document current;

        AbstractDocumentIterator(List<Document> documents, int pos) {
            this.documents = documents;
            this.pos = pos;
        }

        protected abstract Document getNext();

        @Override
        public boolean hasNext() {
            if (current == null) {
                current = getNext();
            }
            return (current != null);
        }

        @Override
        public Document next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            Document document = current;
            current = getNext();
            return document;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

    }

    private static class DocumentIterator extends AbstractDocumentIterator {

        private DocumentIterator(List<Document> documents) {
            super(documents, 0);
        }

        @Override
        protected Document getNext() {
            while (pos < documents.size()) {
                Document document = documents.get(pos++);
                if (document != null) {
                    return document;
                }
            }
            return null;
        }

    }

    private static class ReverseDocumentIterator extends AbstractDocumentIterator {

        private ReverseDocumentIterator(List<Document> documents) {
            super(documents, documents.size() - 1);
        }

        @Override
        protected Document getNext() {
            while (pos >= 0) {
                Document document = documents.get(pos--);
                if (document != null) {
                    return document;
                }
            }
            return null;
        }

    }
}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/backend/aggregation/stage/RedactStageTest.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import static de.bwaldvogel.mongo.TestUtils.json;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;

import de.bwaldvogel.mongo.bson.Document;

class RedactStageTest {

    @Test
    void testRedact() {
        assertThat(redact(
            json("_id: 1"),
            json("$cond: {if: {$eq: [\"$_id\", 1]}, then: \"$$KEEP\", else: \"$$PRUNE\"}"))
        ).contains(json("_id: 1"));

        assertThat(redact(
            json("_id: 1"),
            json("$cond: {if: {$eq: [\"$_id\", 0]}, then: \"$$PRUNE\", else: \"$$KEEP\"}"))
        ).contains(json("_id: 1"));

        assertThat(redact(
            json("_id: 1"),
            json("$cond: {if: {$eq: [\"$_id\", 1]}, then: \"$$PRUNE\", else: \"$$KEEP\"}"))
        ).isEmpty();

        assertThat(redact(
            json("_id: 1"),
            json("$cond: {if: {$eq: [\"$_id\", 0]}, then: \"$$KEEP\", else: \"$$PRUNE\"}"))
        ).isEmpty();
    }

    private static List<Document> redact(Document input, Document redact) {
        return new RedactStage(redact).apply(Stream.of(input)).collect(Collectors.toList());
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/AbstractCursor.java
package de.bwaldvogel.mongo.backend;

public abstract class AbstractCursor implements Cursor {

    protected final long id;

    protected AbstractCursor(long id) {
        this.id = id;
    }

    @Override
    public long getId() {
        return id;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "(id: " + id + ")";
    }
}
<file_sep>/core/src/test/java/de/bwaldvogel/mongo/bson/Decimal128Test.java
package de.bwaldvogel.mongo.bson;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.math.BigDecimal;

import org.junit.jupiter.api.Test;

import nl.jqno.equalsverifier.EqualsVerifier;

class Decimal128Test {

    @Test
    void testEqualsAndHashCodeContract() throws Exception {
        EqualsVerifier.forClass(Decimal128.class).verify();
    }

    @Test
    void testFromAndToBigDecimal() throws Exception {
        assertThat(new Decimal128(new BigDecimal("1.2345")).toBigDecimal().toString()).isEqualTo("1.2345");
        assertThat(new Decimal128(new BigDecimal("1.2345678901234567890123465789")).toBigDecimal().toString()).isEqualTo("1.2345678901234567890123465789");
        assertThat(new Decimal128(new BigDecimal("12345678901234567890123465789")).toBigDecimal().toString()).isEqualTo("12345678901234567890123465789");
        assertThat(new Decimal128(new BigDecimal("-1.2345")).toBigDecimal().toString()).isEqualTo("-1.2345");
        assertThat(new Decimal128(new BigDecimal("0.0")).toBigDecimal().toString()).isEqualTo("0.0");
        assertThat(new Decimal128(new BigDecimal("0").scaleByPowerOfTen(100)).toBigDecimal().toString()).isEqualTo("0E+100");
        assertThat(new Decimal128(new BigDecimal("-0")).toBigDecimal().toString()).isEqualTo("0");
    }

    @Test
    void testFromBigDecimal_NumberFormatException() {
        assertThatExceptionOfType(NumberFormatException.class)
            .isThrownBy(() -> new Decimal128(new BigDecimal("2").scaleByPowerOfTen(10000)))
            .withMessage("Exponent is out of range for Decimal128 encoding of 2E+10000");

        assertThatExceptionOfType(NumberFormatException.class)
            .isThrownBy(() -> new Decimal128(new BigDecimal("1.23456789012345678901234657890123456789")))
            .withMessage("Conversion to Decimal128 would require inexact rounding of 1.23456789012345678901234657890123456789");

        assertThatExceptionOfType(NumberFormatException.class)
            .isThrownBy(() -> new Decimal128(new BigDecimal("2").scaleByPowerOfTen(-10000)))
            .withMessage("Conversion to Decimal128 would require inexact rounding of 2E-10000");
    }

    @Test
    void testToInt() throws Exception {
        assertThat(Decimal128.ONE.intValue()).isEqualTo(1);
        assertThat(Decimal128.POSITIVE_ZERO.intValue()).isEqualTo(0);
        assertThat(Decimal128.NEGATIVE_ZERO.intValue()).isEqualTo(0);

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(Decimal128.NaN::intValue)
            .withMessage("NaN cannot be converted to BigDecimal");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(Decimal128.POSITIVE_INFINITY::intValue)
            .withMessage("Infinity cannot be converted to BigDecimal");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(Decimal128.NEGATIVE_INFINITY::intValue)
            .withMessage("-Infinity cannot be converted to BigDecimal");
    }

    @Test
    void testToLong() throws Exception {
        assertThat(Decimal128.ONE.longValue()).isEqualTo(1L);
        assertThat(Decimal128.POSITIVE_ZERO.longValue()).isEqualTo(0L);
        assertThat(Decimal128.NEGATIVE_ZERO.longValue()).isEqualTo(0L);

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(Decimal128.NaN::longValue)
            .withMessage("NaN cannot be converted to BigDecimal");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(Decimal128.POSITIVE_INFINITY::longValue)
            .withMessage("Infinity cannot be converted to BigDecimal");

        assertThatExceptionOfType(IllegalArgumentException.class)
            .isThrownBy(Decimal128.NEGATIVE_INFINITY::longValue)
            .withMessage("-Infinity cannot be converted to BigDecimal");
    }

    @Test
    void testToFloat() throws Exception {
        assertThat(Decimal128.ONE.floatValue()).isEqualTo(1.0f);
        assertThat(Decimal128.POSITIVE_ZERO.floatValue()).isEqualTo(0.0f);
        assertThat(Decimal128.NEGATIVE_ZERO.floatValue()).isEqualTo(0.0f);
        assertThat(Decimal128.NaN.floatValue()).isNaN();
        assertThat(Decimal128.POSITIVE_INFINITY.floatValue()).isEqualTo(Float.POSITIVE_INFINITY);
        assertThat(Decimal128.NEGATIVE_INFINITY.floatValue()).isEqualTo(Float.NEGATIVE_INFINITY);
    }

    @Test
    void testToDouble() throws Exception {
        assertThat(Decimal128.ONE.doubleValue()).isEqualTo(1.0);
        assertThat(Decimal128.POSITIVE_ZERO.doubleValue()).isEqualTo(0.0);
        assertThat(Decimal128.NEGATIVE_ZERO.doubleValue()).isEqualTo(0.0);
        assertThat(Decimal128.NaN.doubleValue()).isNaN();
        assertThat(Decimal128.POSITIVE_INFINITY.doubleValue()).isEqualTo(Double.POSITIVE_INFINITY);
        assertThat(Decimal128.NEGATIVE_INFINITY.doubleValue()).isEqualTo(Double.NEGATIVE_INFINITY);
    }

    @Test
    void testToString() throws Exception {
        assertThat(new Decimal128(new BigDecimal("1e1000"))).hasToString("1E+1000");
        assertThat(new Decimal128(new BigDecimal("1000"))).hasToString("1000");
        assertThat(new Decimal128(new BigDecimal("1000.987654321"))).hasToString("1000.987654321");
        assertThat(new Decimal128(new BigDecimal("1000").scaleByPowerOfTen(2))).hasToString("1.000E+5");
        assertThat(Decimal128.POSITIVE_INFINITY).hasToString("Infinity");
        assertThat(Decimal128.NEGATIVE_INFINITY).hasToString("-Infinity");
        assertThat(Decimal128.NaN).hasToString("NaN");
    }

}
<file_sep>/test-common/src/main/java/de/bwaldvogel/mongo/backend/AbstractOplogTest.java
package de.bwaldvogel.mongo.backend;

import static com.mongodb.client.model.Aggregates.match;
import static com.mongodb.client.model.Filters.eq;
import static com.mongodb.client.model.Updates.set;
import static com.mongodb.client.model.Updates.unset;
import static de.bwaldvogel.mongo.backend.TestUtils.json;
import static de.bwaldvogel.mongo.backend.TestUtils.toArray;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.UUID;

import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonTimestamp;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import com.mongodb.client.ChangeStreamIterable;
import com.mongodb.client.MongoChangeStreamCursor;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.changestream.ChangeStreamDocument;
import com.mongodb.client.model.changestream.FullDocument;
import com.mongodb.client.result.InsertOneResult;
import com.mongodb.client.result.UpdateResult;

import de.bwaldvogel.mongo.oplog.OperationType;

public abstract class AbstractOplogTest extends AbstractTest {

    protected static final String LOCAL_DATABASE = "local";
    protected static final String OPLOG_COLLECTION_NAME = "oplog.rs";

    @BeforeEach
    void beforeEach() {
        backend.enableOplog();
    }

    @Override
    protected void dropAllDatabases() {
        super.dropAllDatabases();
        clearOplog();
    }

    protected void clearOplog() {
        getOplogCollection().deleteMany(json(""));
    }

    protected MongoCollection<Document> getOplogCollection() {
        MongoDatabase localDb = syncClient.getDatabase(LOCAL_DATABASE);
        return localDb.getCollection(OPLOG_COLLECTION_NAME);
    }

    @Test
    void testListDatabaseNames() throws Exception {
        assertThat(listDatabaseNames()).contains(LOCAL_DATABASE);
        collection.insertOne(json(""));
        assertThat(listDatabaseNames()).containsExactlyInAnyOrder(db.getName(), LOCAL_DATABASE);
        syncClient.getDatabase("bar").getCollection("some-collection").insertOne(json(""));
        assertThat(listDatabaseNames()).containsExactlyInAnyOrder("bar", db.getName(), LOCAL_DATABASE);
    }

    @Test
    void testOplogInsertUpdateAndDelete() {
        Document document = json("_id: 1, name: 'testUser1'");

        collection.insertOne(document);
        clock.windForward(Duration.ofSeconds(1));
        collection.updateOne(json("_id: 1"), json("$set: {name: 'user 2'}"));
        clock.windForward(Duration.ofSeconds(1));
        collection.deleteOne(json("_id: 1"));

        List<Document> oplogDocuments = toArray(getOplogCollection().find().sort(json("ts: 1")));
        assertThat(oplogDocuments).hasSize(3);

        Document insertOplogDocument = oplogDocuments.get(0);
        assertThat(insertOplogDocument).containsKeys("ts", "t", "h", "v", "op", "ns", "ui", "wall", "o");
        assertThat(insertOplogDocument.get("ts")).isInstanceOf(BsonTimestamp.class);
        assertThat(insertOplogDocument.get("t")).isEqualTo(1L);
        assertThat(insertOplogDocument.get("h")).isEqualTo(0L);
        assertThat(insertOplogDocument.get("v")).isEqualTo(2L);
        assertThat(insertOplogDocument.get("op")).isEqualTo(OperationType.INSERT.getCode());
        assertThat(insertOplogDocument.get("ns")).isEqualTo(collection.getNamespace().getFullName());
        assertThat(insertOplogDocument.get("ui")).isInstanceOf(UUID.class);
        assertThat(insertOplogDocument.get("wall")).isEqualTo(Date.from(Instant.parse("2019-05-23T12:00:00.123Z")));
        assertThat(insertOplogDocument.get("o")).isEqualTo(document);

        Document updateOplogDocument = oplogDocuments.get(1);
        assertThat(updateOplogDocument).containsKeys("ts", "t", "h", "v", "op", "ns", "ui", "wall", "o", "o2");
        assertThat(updateOplogDocument.get("ts")).isInstanceOf(BsonTimestamp.class);
        assertThat(updateOplogDocument.get("t")).isEqualTo(1L);
        assertThat(updateOplogDocument.get("h")).isEqualTo(0L);
        assertThat(updateOplogDocument.get("v")).isEqualTo(2L);
        assertThat(updateOplogDocument.get("op")).isEqualTo(OperationType.UPDATE.getCode());
        assertThat(updateOplogDocument.get("ns")).isEqualTo(collection.getNamespace().getFullName());
        assertThat(updateOplogDocument.get("ui")).isInstanceOf(UUID.class);
        assertThat(updateOplogDocument.get("wall")).isEqualTo(Date.from(Instant.parse("2019-05-23T12:00:01.123Z")));
        assertThat(updateOplogDocument.get("o2")).isEqualTo(json("_id: 1"));
        assertThat(updateOplogDocument.get("o")).isEqualTo(json("$set: {name: 'user 2'}"));

        Document deleteOplogDocument = oplogDocuments.get(2);
        assertThat(deleteOplogDocument).containsKeys("ts", "t", "h", "v", "op", "ns", "ui", "wall", "o");
        assertThat(deleteOplogDocument.get("ts")).isInstanceOf(BsonTimestamp.class);
        assertThat(deleteOplogDocument.get("t")).isEqualTo(1L);
        assertThat(deleteOplogDocument.get("h")).isEqualTo(0L);
        assertThat(deleteOplogDocument.get("v")).isEqualTo(2L);
        assertThat(deleteOplogDocument.get("op")).isEqualTo(OperationType.DELETE.getCode());
        assertThat(deleteOplogDocument.get("ns")).isEqualTo(collection.getNamespace().getFullName());
        assertThat(deleteOplogDocument.get("ui")).isInstanceOf(UUID.class);
        assertThat(deleteOplogDocument.get("wall")).isEqualTo(Date.from(Instant.parse("2019-05-23T12:00:02.123Z")));
        assertThat(deleteOplogDocument.get("o")).isEqualTo(json("_id: 1"));
    }

    @Test
    void testQueryOplogWhenOplogIsDisabled() throws Exception {
        backend.disableOplog();
        collection.insertOne(json("_id: 1"));
        assertThat(getOplogCollection().find()).isEmpty();
    }

    @Test
    void testSetOplogReplaceOneById() {
        collection.insertOne(json("_id: 1, b: 6"));
        collection.replaceOne(json("_id: 1"), json("a: 5, b: 7"));
        List<Document> oplogDocuments = toArray(getOplogCollection().find().sort(json("ts: 1")));
        Document updateOplogEntry = oplogDocuments.get(1);
        assertThat(updateOplogEntry.get("op")).isEqualTo(OperationType.UPDATE.getCode());
        assertThat(updateOplogEntry.get("ns")).isEqualTo(collection.getNamespace().toString());
        assertThat(updateOplogEntry.get("o")).isEqualTo(json("_id: 1, a: 5, b: 7"));
        assertThat(updateOplogEntry.get("o2")).isEqualTo(json("_id: 1"));
    }

    @Test
    void testSetOplogUpdateOneById() {
        collection.insertOne(json("_id: 34, b: 6"));
        collection.updateOne(eq("_id", 34), set("a", 6));
        List<Document> oplogDocuments = toArray(getOplogCollection().find(json("op: 'u'")).sort(json("ts: 1")));

        Document updateOplogDocument = CollectionUtils.getSingleElement(oplogDocuments);
        assertThat(updateOplogDocument).containsKeys("ts", "t", "h", "v", "op", "ns", "ui", "wall", "o", "o2");
        assertThat(updateOplogDocument.get("ts")).isInstanceOf(BsonTimestamp.class);
        assertThat(updateOplogDocument.get("t")).isEqualTo(1L);
        assertThat(updateOplogDocument.get("h")).isEqualTo(0L);
        assertThat(updateOplogDocument.get("v")).isEqualTo(2L);
        assertThat(updateOplogDocument.get("op")).isEqualTo(OperationType.UPDATE.getCode());
        assertThat(updateOplogDocument.get("ns")).isEqualTo(collection.getNamespace().getFullName());
        assertThat(updateOplogDocument.get("ui")).isInstanceOf(UUID.class);
        assertThat(updateOplogDocument.get("o2")).isEqualTo(json("_id: 34"));
        assertThat(updateOplogDocument.get("o")).isEqualTo(json("$set: {a: 6}"));
    }

    @Test
    @Disabled("This test represents a missing feature")
    void testSetOplogUpdateOneByIdMultipleFields() {
        collection.insertOne(json("_id: 1, b: 6"));
        collection.updateOne(eq("_id", 1), List.of(set("a", 7), set("b", 7)));
        List<Document> oplogDocuments = toArray(getOplogCollection().find().sort(json("ts: 1")));

        Document updateOplogDocument = oplogDocuments.get(1);
        assertThat(updateOplogDocument).containsKeys("ts", "t", "h", "v", "op", "ns", "ui", "wall", "o", "o2");
        assertThat(updateOplogDocument.get("ts")).isInstanceOf(BsonTimestamp.class);
        assertThat(updateOplogDocument.get("t")).isEqualTo(1L);
        assertThat(updateOplogDocument.get("h")).isEqualTo(0L);
        assertThat(updateOplogDocument.get("v")).isEqualTo(2L);
        assertThat(updateOplogDocument.get("op")).isEqualTo(OperationType.UPDATE.getCode());
        assertThat(updateOplogDocument.get("ns")).isEqualTo(collection.getNamespace().getFullName());
        assertThat(updateOplogDocument.get("ui")).isInstanceOf(UUID.class);
        assertThat(updateOplogDocument.get("o2")).isEqualTo(json("_id: 1"));
        assertThat(updateOplogDocument.get("o")).isEqualTo(json("$set: {a: 7, b: 7}"));
    }

    @Test
    void testSetOplogUpdateMany() {
        collection.insertMany(List.of(json("_id: 1, b: 6"), json("_id: 2, b: 6")));
        collection.updateMany(eq("b", 6), set("a", 7));

        List<Document> oplogDocuments = toArray(getOplogCollection().find(json("op: 'u'")).sort(json("ts: 1, 'o2._id': 1")));
        assertThat(oplogDocuments).hasSize(2);

        for (int i = 0; i < 2; i++) {
            Document updateOplogDocument = oplogDocuments.get(i);
            assertThat(updateOplogDocument).containsKeys("ts", "t", "h", "v", "op", "ns", "ui", "wall", "o", "o2");
            assertThat(updateOplogDocument.get("ts")).isInstanceOf(BsonTimestamp.class);
            assertThat(updateOplogDocument.get("t")).isEqualTo(1L);
            assertThat(updateOplogDocument.get("h")).isEqualTo(0L);
            assertThat(updateOplogDocument.get("v")).isEqualTo(2L);
            assertThat(updateOplogDocument.get("op")).isEqualTo(OperationType.UPDATE.getCode());
            assertThat(updateOplogDocument.get("ns")).isEqualTo(collection.getNamespace().getFullName());
            assertThat(updateOplogDocument.get("ui")).isInstanceOf(UUID.class);
            assertThat(updateOplogDocument.get("o2")).isEqualTo(json(String.format("_id: %d", i + 1)));
            assertThat(updateOplogDocument.get("o")).isEqualTo(json("$set: {a: 7}"));
        }
    }

    @Test
    void testSetOplogDeleteMany() {
        collection.insertMany(List.of(json("_id: 1, b: 6"), json("_id: 2, b: 6")));
        collection.deleteMany(eq("b", 6));

        List<Document> oplogDocuments = toArray(getOplogCollection().find(json("op: 'd'")).sort(json("ts: 1, 'o._id': 1")));
        assertThat(oplogDocuments).hasSize(2);

        for (int i = 0; i < 2; i++) {
            Document updateOplogDocument = oplogDocuments.get(i);
            assertThat(updateOplogDocument).containsKeys("ts", "t", "h", "v", "op", "ns", "ui", "wall", "o");
            assertThat(updateOplogDocument.get("ts")).isInstanceOf(BsonTimestamp.class);
            assertThat(updateOplogDocument.get("t")).isEqualTo(1L);
            assertThat(updateOplogDocument.get("h")).isEqualTo(0L);
            assertThat(updateOplogDocument.get("v")).isEqualTo(2L);
            assertThat(updateOplogDocument.get("op")).isEqualTo(OperationType.DELETE.getCode());
            assertThat(updateOplogDocument.get("ns")).isEqualTo(collection.getNamespace().getFullName());
            assertThat(updateOplogDocument.get("ui")).isInstanceOf(UUID.class);
            assertThat(updateOplogDocument.get("o")).isEqualTo(json(String.format("_id: %d", i + 1)));
        }
    }

    @Test
    void testChangeStreamInsertAndUpdateFullDocumentLookup() {
        collection.insertOne(json("b: 1"));
        int numberOfDocs = 10;
        List<Document> insert = new ArrayList<>();
        List<Document> update = new ArrayList<>();
        List<ChangeStreamDocument<Document>> changeStreamsResult = new ArrayList<>();
        List<Bson> pipeline = List.of(match(Filters.or(
            Document.parse("{'fullDocument.b': 1}")))
        );

        try (MongoChangeStreamCursor<ChangeStreamDocument<Document>> cursor =
                 collection.watch(pipeline).fullDocument(FullDocument.UPDATE_LOOKUP).cursor()) {

            final long cursorId = cursor.getServerCursor().getId();

            for (int i = 1; i < numberOfDocs + 1; i++) {
                Document doc = json(String.format("a: %d, b: 1", i));
                collection.insertOne(doc);
                collection.updateOne(eq("a", i), set("c", i * 10));

                assertThat(cursor.hasNext()).isTrue();
                ChangeStreamDocument<Document> insertDocument = cursor.next();
                assertThat(cursor.getServerCursor().getId()).isEqualTo(cursorId);

                assertThat(cursor.hasNext()).isTrue();
                ChangeStreamDocument<Document> updateDocument = cursor.next();
                assertThat(cursor.getServerCursor().getId()).isEqualTo(cursorId);

                assertThat(insertDocument.getFullDocument().get("a")).isEqualTo(i);
                insert.add(insertDocument.getFullDocument());

                assertThat(updateDocument.getFullDocument().get("a")).isEqualTo(i);
                update.add(updateDocument.getFullDocument());

                changeStreamsResult.addAll(List.of(insertDocument, updateDocument));
            }
        }

        assertThat(insert.size()).isEqualTo(numberOfDocs);
        assertThat(update.size()).isEqualTo(numberOfDocs);
        assertThat(changeStreamsResult.size()).isEqualTo(numberOfDocs * 2);
    }

    @Test
    void testChangeStreamUpdateDefault() {
        collection.insertOne(json("a: 1, b: 2, c: 3"));
        try (MongoChangeStreamCursor<ChangeStreamDocument<Document>> cursor = collection.watch().cursor()) {
            collection.updateOne(eq("a", 1), json("$set: {b: 0, c: 10}"));
            ChangeStreamDocument<Document> updateDocument = cursor.next();
            Document fullDoc = updateDocument.getFullDocument();
            assertThat(fullDoc).isNotNull();
            assertThat(fullDoc.get("b")).isEqualTo(0);
            assertThat(fullDoc.get("c")).isEqualTo(10);

            collection.updateOne(eq("a", 1), unset("b"));
            updateDocument = cursor.next();
            fullDoc = updateDocument.getFullDocument();
            assertThat(fullDoc).isNotNull();
            assertThat(fullDoc.get("b")).isEqualTo("");
        }
    }

    @Test
    void testChangeStreamDelete() {
        collection.insertOne(json("_id: 1"));
        try (MongoChangeStreamCursor<ChangeStreamDocument<Document>> cursor = collection.watch().cursor()) {
            collection.deleteOne(json("_id: 1"));
            ChangeStreamDocument<Document> deleteDocument = cursor.next();
            assertThat(deleteDocument.getDocumentKey().get("_id")).isEqualTo(new BsonInt32(1));
        }
    }

    @Test
    void testChangeStreamStartAfter() {
        collection.insertOne(json("a: 1")); // This is needed to initialize the collection in the server.
        try (MongoChangeStreamCursor<ChangeStreamDocument<Document>> cursor = collection.watch().cursor()) {
            collection.insertOne(json("a: 2"));
            collection.insertOne(json("a: 3"));
            ChangeStreamDocument<Document> document = cursor.next();
            BsonDocument resumeToken = document.getResumeToken();

            try (MongoChangeStreamCursor<ChangeStreamDocument<Document>> cursor2
                     = collection.watch().startAfter(resumeToken).cursor()) {
                ChangeStreamDocument<Document> document2 = cursor2.next();
                assertThat(document2.getFullDocument().get("a")).isEqualTo(3);
            }
        }
    }

    @Test
    void testChangeStreamResumeAfter() throws Exception {
        collection.insertOne(json("a: 1"));
        try (MongoChangeStreamCursor<ChangeStreamDocument<Document>> cursor = collection.watch().cursor()) {
            awaitNumberOfOpenCursors(1);
            collection.insertOne(json("a: 2"));
            collection.insertOne(json("a: 3"));
            ChangeStreamDocument<Document> document = cursor.next();
            BsonDocument resumeToken = document.getResumeToken();

            try (MongoChangeStreamCursor<ChangeStreamDocument<Document>> cursor2
                     = collection.watch().resumeAfter(resumeToken).cursor()) {
                awaitNumberOfOpenCursors(2);
                ChangeStreamDocument<Document> document2 = cursor2.next();
                assertThat(document2.getFullDocument().get("a")).isEqualTo(3);
            }
        }
    }

    @Test
    void testChangeStreamResumeAfterTerminalEvent() {
        MongoCollection<Document> col = db.getCollection("test-collection");
        ChangeStreamIterable<Document> watch = col.watch().fullDocument(FullDocument.UPDATE_LOOKUP).batchSize(1);
        try (MongoChangeStreamCursor<ChangeStreamDocument<Document>> cursor = watch.cursor()) {
            col.insertOne(json("a: 1"));
            cursor.next();

            col.drop();

            ChangeStreamDocument<Document> document = cursor.next();
            BsonDocument resumeToken = document.getResumeToken();
            try (MongoChangeStreamCursor<ChangeStreamDocument<Document>> resumeAfterCursor
                     = watch.resumeAfter(resumeToken).cursor();) {
                document = resumeAfterCursor.next();

                assertThat(document).isNotNull();
                assertThat(document.getOperationType())
                    .isEqualTo(com.mongodb.client.model.changestream.OperationType.INVALIDATE);

                assertThatExceptionOfType(NoSuchElementException.class)
                    .isThrownBy(resumeAfterCursor::next);
            }
        }
    }

    @Test
    void testChangeStreamStartAtOperationTime() {
        collection.insertOne(json("a: 1"));
        try (MongoChangeStreamCursor<ChangeStreamDocument<Document>> cursor = collection.watch().cursor()) {
            collection.insertOne(json("a: 2"));
            collection.insertOne(json("a: 3"));
            ChangeStreamDocument<Document> document = cursor.next();
            BsonTimestamp startAtOperationTime = document.getClusterTime();

            try (MongoChangeStreamCursor<ChangeStreamDocument<Document>> cursor2 = collection.watch().startAtOperationTime(startAtOperationTime).cursor()) {
                ChangeStreamDocument<Document> document2 = cursor2.next();
                assertThat(document2.getFullDocument().get("a")).isEqualTo(2);
                document2 = cursor2.next();
                assertThat(document2.getFullDocument().get("a")).isEqualTo(3);
            }
        }
    }

    @Test
    void testChangeStreamAndReplaceOneWithUpsertTrue() throws Exception {
        TestSubscriber<ChangeStreamDocument<Document>> streamSubscriber = new TestSubscriber<>();
        asyncCollection.watch().fullDocument(FullDocument.UPDATE_LOOKUP).subscribe(streamSubscriber);
        awaitNumberOfOpenCursors(1);

        TestSubscriber<UpdateResult> replaceOneSubscriber = new TestSubscriber<>();
        asyncCollection.replaceOne(json("a: 1"), json("a: 1"), new ReplaceOptions().upsert(true))
            .subscribe(replaceOneSubscriber);

        replaceOneSubscriber.awaitSingleValue();

        TestSubscriber<Document> findSubscriber = new TestSubscriber<>();
        asyncCollection.find(json("a:1")).subscribe(findSubscriber);
        assertThat(findSubscriber.awaitSingleValue().get("a")).isEqualTo(1);

        ChangeStreamDocument<Document> value = streamSubscriber.awaitSingleValue();
        assertThat(value.getOperationType().getValue()).isEqualTo("insert");
        assertThat(value.getFullDocument()).isEqualTo(findSubscriber.awaitSingleValue());
    }

    @Test
    void testSimpleChangeStreamWithFilter() throws Exception {
        insertOne(asyncCollection, json("_id: 1"));

        Bson filter = match(Filters.eq("fullDocument.bu", "abc"));
        List<Bson> pipeline = List.of(filter);

        super.assertNoOpenCursors();
        TestSubscriber<ChangeStreamDocument<Document>> streamSubscriber = new TestSubscriber<>();
        asyncCollection.watch(pipeline).subscribe(streamSubscriber);
        awaitNumberOfOpenCursors(1);

        insertOne(asyncCollection, json("_id: 2, bu: 'abc'"));
        insertOne(asyncCollection, json("_id: 3, bu: 'xyz'"));

        ChangeStreamDocument<Document> changeStreamDocument = streamSubscriber.awaitSingleValue();
        assertThat(changeStreamDocument.getFullDocument().get("bu")).isEqualTo("abc");
    }

    @Test
    void testOplogSubscription() throws Exception {
        super.assertNoOpenCursors();
        TestSubscriber<ChangeStreamDocument<Document>> streamSubscriber = new TestSubscriber<>();
        asyncCollection.watch().subscribe(streamSubscriber);
        awaitNumberOfOpenCursors(1);

        insertOne(asyncCollection, json("_id: 1"));

        ChangeStreamDocument<Document> changeStreamDocument = streamSubscriber.awaitSingleValue();
        assertThat(changeStreamDocument.getOperationType()).isEqualTo(com.mongodb.client.model.changestream.OperationType.INSERT);
        assertThat(changeStreamDocument.getFullDocument()).isEqualTo(json("_id: 1"));
    }

    @Test
    void testOplogShouldFilterNamespaceOnChangeStreams() throws Exception {
        com.mongodb.reactivestreams.client.MongoCollection<Document> asyncCollection1 =
            asyncDb.getCollection(asyncCollection.getNamespace().getCollectionName() + "1");

        insertOne(asyncCollection, json("_id: 1"));
        insertOne(asyncCollection1, json("_id: 1"));

        super.assertNoOpenCursors();
        TestSubscriber<ChangeStreamDocument<Document>> streamSubscriber = new TestSubscriber<>();
        asyncCollection.watch().subscribe(streamSubscriber);
        awaitNumberOfOpenCursors(1);

        insertOne(asyncCollection1, json("_id: 2"));
        insertOne(asyncCollection, json("_id: 2"));

        streamSubscriber.awaitSingleValue();
    }

    private static void insertOne(com.mongodb.reactivestreams.client.MongoCollection<Document> collection, Document document) throws Exception {
        TestSubscriber<InsertOneResult> insertSubscriber = new TestSubscriber<>();
        collection.insertOne(document).subscribe(insertSubscriber);
        insertSubscriber.awaitSingleValue();
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/VisibleForExternalBackends.java
package de.bwaldvogel.mongo.backend;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.SOURCE)
@interface VisibleForExternalBackends {
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/NamespaceExistsException.java
package de.bwaldvogel.mongo.exception;

public class NamespaceExistsException extends MongoServerError {

    private static final long serialVersionUID = 1L;

    public NamespaceExistsException(String message) {
        super(ErrorCode.NamespaceExists, message);
    }

}
<file_sep>/examples/build.gradle
dependencies {
    api project(':mongo-java-server-memory-backend')

    api 'org.mongodb:mongo-java-driver:latest.release'

    api group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: 'latest.release'
    api group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: 'latest.release'
    api group: 'org.assertj', name: 'assertj-core', version: 'latest.release'

    runtimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: '[1.3.0, 1.4.0)'
}

sourceSets {
    test {
        java.srcDirs += [
            sourceSets.main.allJava
        ]
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/bson/LegacyUUID.java
package de.bwaldvogel.mongo.bson;

import java.util.Objects;
import java.util.UUID;

public final class LegacyUUID implements Comparable<LegacyUUID>, Bson {

    private static final long serialVersionUID = 1L;

    private final UUID uuid;

    public LegacyUUID(UUID uuid) {
        this.uuid = Objects.requireNonNull(uuid);
    }

    public LegacyUUID(long mostSigBits, long leastSigBits) {
        this(new UUID(mostSigBits, leastSigBits));
    }

    public UUID getUuid() {
        return uuid;
    }

    public static LegacyUUID fromString(String value) {
        return new LegacyUUID(UUID.fromString(value));
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        LegacyUUID that = (LegacyUUID) o;
        return getUuid().equals(that.getUuid());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getUuid());
    }

    @Override
    public int compareTo(LegacyUUID o) {
        return getUuid().compareTo(o.getUuid());
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/stage/AggregationStage.java
package de.bwaldvogel.mongo.backend.aggregation.stage;

import java.util.stream.Stream;

import de.bwaldvogel.mongo.bson.Document;

public interface AggregationStage {

    String name();

    Stream<Document> apply(Stream<Document> stream);

    default boolean isModifying() {
        return false;
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/AbstractUniqueIndex.java
package de.bwaldvogel.mongo.backend;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.bwaldvogel.mongo.MongoCollection;
import de.bwaldvogel.mongo.bson.BsonRegularExpression;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.DuplicateKeyError;
import de.bwaldvogel.mongo.exception.KeyConstraintError;

public abstract class AbstractUniqueIndex<P> extends Index<P> {

    private static final Logger log = LoggerFactory.getLogger(AbstractUniqueIndex.class);

    protected AbstractUniqueIndex(String name, List<IndexKey> keys, boolean sparse) {
        super(name, keys, sparse);
    }

    protected abstract P removeDocument(KeyValue keyValue);

    protected boolean containsKey(KeyValue keyValue) {
        return getPosition(keyValue) != null;
    }

    protected abstract boolean putKeyPosition(KeyValue keyValue, P position);

    protected abstract Iterable<Entry<KeyValue, P>> getIterable();

    protected abstract P getPosition(KeyValue keyValue);

    private boolean isSparseAndHasNoValueForKeys(Document document) {
        return isSparse() && hasNoValueForKeys(document);
    }

    private boolean hasNoValueForKeys(Document document) {
        return keys().stream().noneMatch(key -> Utils.hasSubdocumentValue(document, key));
    }

    @Override
    public synchronized P remove(Document document) {
        if (isSparseAndHasNoValueForKeys(document)) {
            return null;
        }

        return apply(document, this::removeDocument);
    }

    @Override
    public P getPosition(Document document) {
        return apply(document, this::getPosition);
    }

    private P apply(Document document, Function<KeyValue, P> keyToPositionFunction) {
        Set<KeyValue> keyValues = getKeyValues(document);
        Set<P> positions = keyValues.stream()
            .map(keyToPositionFunction)
            .filter(Objects::nonNull)
            .collect(Collectors.toSet());

        if (positions.isEmpty()) {
            return null;
        } else {
            return CollectionUtils.getSingleElement(positions);
        }
    }

    @Override
    public synchronized void checkAdd(Document document, MongoCollection<P> collection) {
        if (isSparseAndHasNoValueForKeys(document)) {
            return;
        }
        for (KeyValue key : getKeyValues(document, false)) {
            KeyValue normalizedKey = key.normalized();
            if (containsKey(normalizedKey)) {
                throw new DuplicateKeyError(this, collection, getKeys(), key);
            }
        }
    }

    @Override
    public synchronized void add(Document document, P position, MongoCollection<P> collection) {
        checkAdd(document, collection);
        if (isSparseAndHasNoValueForKeys(document)) {
            return;
        }
        Set<KeyValue> keyValues = getKeyValues(document);
        for (KeyValue keyValue : keyValues) {
            boolean added = putKeyPosition(keyValue, position);
            Assert.isTrue(added, () -> "Key " + keyValue + " already exists. Concurrency issue?");
        }
    }

    @Override
    public synchronized void checkUpdate(Document oldDocument, Document newDocument, MongoCollection<P> collection) {
        if (nullAwareEqualsKeys(oldDocument, newDocument)) {
            return;
        }
        if (isSparseAndHasNoValueForKeys(newDocument)) {
            return;
        }
        P oldPosition = getDocumentPosition(oldDocument);
        for (KeyValue key : getKeyValues(newDocument, false)) {
            KeyValue normalizedKey = key.normalized();
            P position = getPosition(normalizedKey);
            if (position != null && !position.equals(oldPosition)) {
                throw new DuplicateKeyError(this, collection, getKeys(), key);
            }
        }
    }

    private P getDocumentPosition(Document oldDocument) {
        Set<P> positions = getKeyValues(oldDocument).stream()
            .map(this::getPosition)
            .collect(StreamUtils.toLinkedHashSet());
        return CollectionUtils.getSingleElement(positions);
    }

    @Override
    public void updateInPlace(Document oldDocument, Document newDocument, P position, MongoCollection<P> collection) throws KeyConstraintError {
        if (!nullAwareEqualsKeys(oldDocument, newDocument)) {
            P removedPosition = remove(oldDocument);
            if (removedPosition != null) {
                Assert.equals(removedPosition, position);
            }
            add(newDocument, position, collection);
        }
    }

    @Override
    public synchronized boolean canHandle(Document query) {

        if (!query.keySet().equals(keySet())) {
            return false;
        }

        if (isSparse() && query.values().stream().allMatch(Objects::isNull)) {
            return false;
        }

        for (String key : keys()) {
            Object queryValue = query.get(key);
            if (queryValue instanceof Document) {
                if (isCompoundIndex()) {
                    // https://github.com/bwaldvogel/mongo-java-server/issues/80
                    // Not yet supported. Use some other index, or none:
                    return false;
                }
                if (BsonRegularExpression.isRegularExpression(queryValue)) {
                    return true;
                }
                for (String queriedKeys : ((Document) queryValue).keySet()) {
                    if (isInQuery(queriedKeys)) {
                        // okay
                    } else if (queriedKeys.startsWith("$")) {
                        // not yet supported
                        return false;
                    }
                }
            }
        }

        return true;
    }

    private static boolean isInQuery(String key) {
        return key.equals(QueryOperator.IN.getValue());
    }

    @Override
    public synchronized Iterable<P> getPositions(Document query) {
        KeyValue queriedKeyValues = getQueriedKeyValues(query);

        for (Object queriedValue : queriedKeyValues) {
            if (BsonRegularExpression.isRegularExpression(queriedValue)) {
                if (isCompoundIndex()) {
                    throw new UnsupportedOperationException("Not yet implemented");
                }
                List<P> positions = new ArrayList<>();
                for (Entry<KeyValue, P> entry : getIterable()) {
                    KeyValue obj = entry.getKey();
                    if (obj.size() == 1) {
                        Object o = obj.get(0);
                        if (o instanceof String) {
                            BsonRegularExpression regularExpression = BsonRegularExpression.convertToRegularExpression(queriedValue);
                            Matcher matcher = regularExpression.matcher(o.toString());
                            if (matcher.find()) {
                                positions.add(entry.getValue());
                            }
                        }
                    }
                }
                return positions;
            } else if (queriedValue instanceof Document) {
                if (isCompoundIndex()) {
                    throw new UnsupportedOperationException("Not yet implemented");
                }
                Document keyObj = (Document) queriedValue;
                if (Utils.containsQueryExpression(keyObj)) {
                    String expression = CollectionUtils.getSingleElement(keyObj.keySet(),
                        () -> new UnsupportedOperationException("illegal query key: " + queriedKeyValues));

                    if (expression.startsWith("$")) {
                        return getPositionsForExpression(keyObj, expression);
                    }
                }
            } else if (queriedValue instanceof Collection) {
                Collection<?> values = (Collection<?>) queriedValue;
                return values.stream()
                    .map(KeyValue::new)
                    .map(this::getPosition)
                    .filter(Objects::nonNull)
                    .collect(StreamUtils.toLinkedHashSet());
            }
        }

        P position = getPosition(queriedKeyValues);
        if (position == null) {
            return List.of();
        }
        return List.of(position);
    }

    private KeyValue getQueriedKeyValues(Document query) {
        return new KeyValue(keys().stream()
            .map(query::get)
            .map(Utils::normalizeValue)
            .collect(Collectors.toList()));
    }

    private Iterable<P> getPositionsForExpression(Document keyObj, String operator) {
        if (isInQuery(operator)) {
            @SuppressWarnings("unchecked")
            Collection<Object> objects = (Collection<Object>) keyObj.get(operator);
            Collection<Object> queriedObjects = new TreeSet<>(ValueComparator.asc());
            queriedObjects.addAll(objects);

            List<P> allKeys = new ArrayList<>();
            for (Object object : queriedObjects) {
                Object keyValue = Utils.normalizeValue(object);
                P key = getPosition(new KeyValue(keyValue));
                if (key != null) {
                    allKeys.add(key);
                }
            }

            return allKeys;
        } else {
            throw new UnsupportedOperationException("unsupported query expression: " + operator);
        }
    }

    @Override
    public boolean isUnique() {
        return true;
    }

    @Override
    public void drop() {
        log.debug("Dropping {}", this);
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/KeyValue.java
package de.bwaldvogel.mongo.backend;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import de.bwaldvogel.mongo.bson.Json;

public final class KeyValue implements Serializable, Iterable<Object> {

    private static final long serialVersionUID = 1L;

    private final List<Object> values;

    public KeyValue(Object... values) {
        this(Arrays.asList(values));
    }

    public KeyValue(Collection<?> values) {
        Assert.notEmpty(values);
        this.values = new ArrayList<>(values);
    }

    public int size() {
        return values.size();
    }

    public Object get(int index) {
        return values.get(index);
    }

    @Override
    public Iterator<Object> iterator() {
        return values.iterator();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        KeyValue keyValue = (KeyValue) o;
        return Objects.equals(values, keyValue.values);
    }

    @Override
    public int hashCode() {
        return Objects.hash(values);
    }

    @Override
    public String toString() {
        return values.stream()
            .map(value -> ": " + Json.toCompactJsonValue(value))
            .collect(Collectors.joining(", ", "{ ", " }"));
    }

    public Stream<Object> stream() {
        return values.stream();
    }

    public KeyValue normalized() {
        return new KeyValue(values.stream()
            .map(Utils::normalizeValue)
            .collect(Collectors.toList()));
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/ExpressionTraits.java
package de.bwaldvogel.mongo.backend.aggregation;

import static de.bwaldvogel.mongo.backend.Missing.isNullOrMissing;
import static de.bwaldvogel.mongo.backend.Utils.describeType;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.function.IntPredicate;

import de.bwaldvogel.mongo.backend.CollectionUtils;
import de.bwaldvogel.mongo.backend.ValueComparator;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.bson.ObjectId;
import de.bwaldvogel.mongo.exception.ErrorCode;
import de.bwaldvogel.mongo.exception.MongoServerError;

interface ExpressionTraits {

    String name();

    default Object requireSingleValue(List<?> list) {
        requireCollectionInSize(list, 1);
        return CollectionUtils.getSingleElement(list);
    }

    default String requireSingleStringValue(List<?> expressionValue) {
        Object value = requireSingleValue(expressionValue);
        if (!(value instanceof String)) {
            throw new MongoServerError(ErrorCode._34471, name() + " requires a string argument, found: " + describeType(value));
        }
        return (String) value;
    }

    default Number evaluateNumericValue(List<?> expressionValue, Function<Double, ? extends Number> function) {
        Object value = requireSingleValue(expressionValue);
        if (isNullOrMissing(value)) {
            return null;
        }
        if (!(value instanceof Number)) {
            throw new MongoServerError(28765,
                name() + " only supports numeric types, not " + describeType(value));
        }
        Number number = (Number) value;
        if (Double.isNaN(number.doubleValue())) {
            return number;
        }
        return function.apply(number.doubleValue());
    }

    default int evaluateComparison(List<?> expressionValue) {
        TwoParameters parameters = requireTwoParameters(expressionValue);
        return ValueComparator.ascWithoutListHandling().compare(parameters.getFirst(), parameters.getSecond());
    }

    default boolean evaluateComparison(List<?> expressionValue, IntPredicate comparison) {
        int comparisonResult = evaluateComparison(expressionValue);
        return comparison.test(comparisonResult);
    }

    default <T> T evaluateDateTime(List<?> expressionValue, Function<ZonedDateTime, T> dateFunction, Document document) {
        Object value = requireSingleValue(expressionValue);
        if (isNullOrMissing(value)) {
            return null;
        }

        ZonedDateTime zonedDateTime = getZonedDateTime(value, document);
        return dateFunction.apply(zonedDateTime);
    }

    default <T> T evaluateDate(List<?> expressionValue, Function<LocalDate, T> dateFunction, Document document) {
        return evaluateDateTime(expressionValue, zonedDateTime -> dateFunction.apply(zonedDateTime.toLocalDate()), document);
    }

    default <T> T evaluateTime(List<?> expressionValue, Function<LocalTime, T> timeFunction, Document document) {
        return evaluateDateTime(expressionValue, zonedDateTime -> timeFunction.apply(zonedDateTime.toLocalTime()), document);
    }

    default void requireCollectionInSize(List<?> value, int expectedCollectionSize) {
        if (value.size() != expectedCollectionSize) {
            throw new MongoServerError(16020, "Expression " + name() + " takes exactly " + expectedCollectionSize + " arguments. " + value.size() + " were passed in.");
        }
    }

    default TwoParameters requireTwoParameters(List<?> parameters) {
        requireCollectionInSize(parameters, 2);
        return new TwoParameters(parameters.get(0), parameters.get(1));
    }

    default TwoNumericParameters requireTwoNumericParameters(List<?> value, int errorCode) {
        TwoParameters parameters = requireTwoParameters(value);
        Object one = parameters.getFirst();
        Object other = parameters.getSecond();

        if (parameters.isAnyNull()) {
            return null;
        }

        if (!(one instanceof Number && other instanceof Number)) {
            throw new MongoServerError(errorCode,
                name() + " only supports numeric types, not " + describeType(one) + " and " + describeType(other));
        }

        return new TwoNumericParameters((Number) one, (Number) other);
    }

    default ZonedDateTime getZonedDateTime(Object value, Document document) {
        ZoneId timezone = ZoneId.systemDefault();
        if (value instanceof Document) {
            Document valueAsDocument = (Document) value;
            if (!valueAsDocument.containsKey("date")) {
                throw new MongoServerError(40539, "missing 'date' argument to " + name() + ", provided: " + value);
            }
            value = Expression.evaluate(valueAsDocument.get("date"), document);

            Object timezoneExpression = Expression.evaluate(valueAsDocument.get("timezone"), document);
            if (timezoneExpression != null) {
                timezone = ZoneId.of(timezoneExpression.toString());
            }
        }
        if (!(value instanceof Instant)) {
            throw new MongoServerError(16006, "can't convert from " + describeType(value) + " to Date");
        }

        Instant instant = (Instant) value;
        return ZonedDateTime.ofInstant(instant, timezone);
    }

    default int requireIntegral(Object value, String name) {
        if (!(value instanceof Number)) {
            throw new MongoServerError(40096,
                name() + " requires an integral " + name + ", found a value of type: " + describeType(value) + ", with value: \"" + value + "\"");
        }
        Number number = (Number) value;
        int intValue = number.intValue();
        if (intValue < 0) {
            throw new MongoServerError(40097, name() + " requires a nonnegative " + name + ", found: " + intValue);
        }
        return intValue;
    }

    default Object assertTwoToFourArguments(List<?> expressionValue) {
        if (expressionValue.size() < 2 || expressionValue.size() > 4) {
            throw new MongoServerError(28667,
                "Expression " + name() + " takes at least 2 arguments, and at most 4, but " + expressionValue.size() + " were passed in.");
        }

        Object first = expressionValue.get(0);
        if (isNullOrMissing(first)) {
            return null;
        }
        return first;
    }

    default <T> Object evaluateIndexOf(List<?> expressionValue,
                                       Function<String, List<T>> toList,
                                       int errorCodeFirstParameterTypeMismatch,
                                       int errorCodeSecondParameterTypeMismatch) {
        Object first = assertTwoToFourArguments(expressionValue);
        if (first == null) {
            return null;
        }
        if (!(first instanceof String)) {
            throw new MongoServerError(errorCodeFirstParameterTypeMismatch,
                name() + " requires a string as the first argument, found: " + describeType(first));
        }
        List<T> elementsToSearchIn = toList.apply((String) first);

        Object searchValue = expressionValue.get(1);
        if (!(searchValue instanceof String)) {
            throw new MongoServerError(errorCodeSecondParameterTypeMismatch,
                name() + " requires a string as the second argument, found: " + describeType(searchValue));
        }
        List<T> search = toList.apply((String) searchValue);

        Range range = indexOf(expressionValue, elementsToSearchIn.size());

        elementsToSearchIn = elementsToSearchIn.subList(range.getStart(), range.getEnd());
        int index = Collections.indexOfSubList(elementsToSearchIn, search);
        if (index >= 0) {
            return index + range.getStart();
        }
        return index;
    }

    default Range indexOf(List<?> expressionValue, int size) {
        int start = 0;
        if (expressionValue.size() >= 3) {
            Object startValue = expressionValue.get(2);
            start = requireIntegral(startValue, "starting index");
            start = Math.min(start, size);
        }

        int end = size;
        if (expressionValue.size() >= 4) {
            Object endValue = expressionValue.get(3);
            end = requireIntegral(endValue, "ending index");
            end = Math.min(Math.max(start, end), size);
        }
        return new Range(start, end);
    }

    default Collection<?> requireArray(int errorCode, Object value) {
        if (!(value instanceof Collection)) {
            throw new MongoServerError(errorCode,
                "The argument to " + name() + " must be an array, but was of type: " + describeType(value));
        }
        return (Collection<?>) value;
    }

    default Document requireDocument(Object expressionValue, int errorCode) {
        if (!(expressionValue instanceof Document)) {
            throw new MongoServerError(errorCode, name() + " only supports an object as its argument");
        }
        return (Document) expressionValue;
    }

    default String evaluateString(List<?> expressionValue, Function<String, String> function) {
        Object value = requireSingleValue(expressionValue);
        value = convertToString(value);
        if (value == null) return null;
        return function.apply((String) value);
    }

    default String convertToString(Object value) {
        if (isNullOrMissing(value)) {
            return null;
        } else if (value instanceof String) {
            return (String) value;
        } else if (value instanceof Number) {
            return value.toString();
        } else if (value instanceof ObjectId) {
            return ((ObjectId) value).getHexData();
        }
        throw new MongoServerError(16007, "can't convert from BSON type " + describeType(value) + " to String");
    }

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/IndexKey.java
package de.bwaldvogel.mongo.backend;

public class IndexKey {

    private final String key;
    private final boolean ascending;

    public IndexKey(String key, boolean ascending) {
        this.key = key;
        this.ascending = ascending;
    }

    public String getKey() {
        return key;
    }

    public boolean isAscending() {
        return ascending;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[key=" + key + " " + (ascending ? "ASC" : "DESC") + "]";
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/MongoBackend.java
package de.bwaldvogel.mongo;

import java.time.Clock;
import java.util.Collection;
import java.util.List;

import de.bwaldvogel.mongo.backend.QueryResult;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.wire.message.MongoMessage;
import de.bwaldvogel.mongo.wire.message.MongoQuery;
import io.netty.channel.Channel;

public interface MongoBackend {

    void handleClose(Channel channel);

    Document handleCommand(Channel channel, String database, String command, Document query);

    QueryResult handleQuery(MongoQuery query);

    Document handleMessage(MongoMessage message);

    void dropDatabase(String database);

    Collection<Document> getCurrentOperations(MongoQuery query);

    Document getServerStatus();

    void close();

    void closeCursors(List<Long> cursorIds);

    Clock getClock();

    void enableOplog();

    void disableOplog();

    MongoDatabase resolveDatabase(String database);

    MongoBackend version(ServerVersion version);

}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/bson/BsonJavaScript.java
package de.bwaldvogel.mongo.bson;

import java.util.Objects;

public final class BsonJavaScript implements Bson {

    private static final long serialVersionUID = 1L;

    private final String code;

    public BsonJavaScript(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        BsonJavaScript that = (BsonJavaScript) o;
        return Objects.equals(getCode(), that.getCode());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getCode());
    }


}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/EmptyIndex.java
package de.bwaldvogel.mongo.backend;

import java.util.List;

import de.bwaldvogel.mongo.MongoCollection;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.KeyConstraintError;

public class EmptyIndex<P> extends Index<P> {

    public EmptyIndex(String name, List<IndexKey> keys) {
        super(name, keys, true);
    }

    @Override
    public P getPosition(Document document) {
        return null;
    }

    @Override
    public void checkAdd(Document document, MongoCollection<P> collection) {
        // ignore
    }

    @Override
    public void add(Document document, P position, MongoCollection<P> collection) {
        // ignore
    }

    @Override
    public P remove(Document document) {
        return null;
    }

    @Override
    public boolean canHandle(Document query) {
        return false;
    }

    @Override
    public Iterable<P> getPositions(Document query) {
        throw new UnsupportedOperationException();
    }

    @Override
    public long getCount() {
        return 0;
    }

    @Override
    public long getDataSize() {
        return 0;
    }

    @Override
    public void checkUpdate(Document oldDocument, Document newDocument, MongoCollection<P> collection) {
        // ignore
    }

    @Override
    public void updateInPlace(Document oldDocument, Document newDocument, P position, MongoCollection<P> collection) throws KeyConstraintError {
        // ignore
    }

    @Override
    public void drop() {
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/Aggregation.java
package de.bwaldvogel.mongo.backend.aggregation;

import static de.bwaldvogel.mongo.backend.Constants.ID_FIELD;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.bwaldvogel.mongo.MongoCollection;
import de.bwaldvogel.mongo.MongoDatabase;
import de.bwaldvogel.mongo.backend.Assert;
import de.bwaldvogel.mongo.backend.CollectionUtils;
import de.bwaldvogel.mongo.backend.DatabaseResolver;
import de.bwaldvogel.mongo.backend.aggregation.stage.AddFieldsStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.AggregationStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.BucketStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.FacetStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.GraphLookupStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.GroupStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.IndexStatsStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.LimitStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.LookupStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.LookupWithPipelineStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.MatchStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.MergeStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.OutStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.ProjectStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.RedactStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.ReplaceRootStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.SampleStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.SkipStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.SortStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.TerminalStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.UnsetStage;
import de.bwaldvogel.mongo.backend.aggregation.stage.UnwindStage;
import de.bwaldvogel.mongo.bson.Document;
import de.bwaldvogel.mongo.exception.ErrorCode;
import de.bwaldvogel.mongo.exception.FailedToOptimizePipelineError;
import de.bwaldvogel.mongo.exception.FailedToParseException;
import de.bwaldvogel.mongo.exception.MongoServerError;
import de.bwaldvogel.mongo.exception.MongoServerNotYetImplementedException;
import de.bwaldvogel.mongo.exception.TypeMismatchException;
import de.bwaldvogel.mongo.oplog.Oplog;

public class Aggregation {

    private static final Logger log = LoggerFactory.getLogger(Aggregation.class);

    private final MongoCollection<?> collection;

    private final List<AggregationStage> stages = new ArrayList<>();
    private Map<String, Object> variables = Collections.emptyMap();

    private Aggregation(MongoCollection<?> collection) {
        this.collection = collection;
    }

    public static List<Document> parse(Object pipelineObject) {
        if (!(pipelineObject instanceof List)) {
            throw new TypeMismatchException("'pipeline' option must be specified as an array");
        }
        List<Document> pipeline = new ArrayList<>();
        for (Object pipelineElement : (List<?>) pipelineObject) {
            if (!(pipelineElement instanceof Document)) {
                throw new TypeMismatchException("Each element of the 'pipeline' array must be an object");
            }
            pipeline.add((Document) pipelineElement);
        }
        return pipeline;
    }

    public static Aggregation fromPipeline(Object pipelineObject, DatabaseResolver databaseResolver,
                                           MongoDatabase database, MongoCollection<?> collection, Oplog oplog) {
        List<Document> pipeline = parse(pipelineObject);
        return fromPipeline(pipeline, databaseResolver, database, collection, oplog);
    }

    public static Aggregation fromPipeline(List<Document> pipeline, DatabaseResolver databaseResolver,
                                           MongoDatabase database, MongoCollection<?> collection, Oplog oplog) {
        Aggregation aggregation = new Aggregation(collection);

        for (Document stage : pipeline) {
            String stageOperation = CollectionUtils.getSingleElement(stage.keySet(), () -> {
                throw new MongoServerError(40323, "A pipeline stage specification object must contain exactly one field.");
            });
            switch (stageOperation) {
                case "$match":
                    Document matchQuery = (Document) stage.get(stageOperation);
                    aggregation.addStage(new MatchStage(matchQuery));
                    break;
                case "$skip":
                    Number numSkip = (Number) stage.get(stageOperation);
                    aggregation.addStage(new SkipStage(numSkip.longValue()));
                    break;
                case "$limit":
                    Number numLimit = (Number) stage.get(stageOperation);
                    aggregation.addStage(new LimitStage(numLimit.longValue()));
                    break;
                case "$sort":
                    Document orderBy = (Document) stage.get(stageOperation);
                    aggregation.addStage(new SortStage(orderBy));
                    break;
                case "$project":
                    Object projection = stage.get(stageOperation);
                    aggregation.addStage(new ProjectStage(projection));
                    break;
                case "$count":
                    String count = (String) stage.get(stageOperation);
                    aggregation.addStage(new GroupStage(new Document(ID_FIELD, null).append(count, new Document("$sum", 1))));
                    aggregation.addStage(new ProjectStage(new Document(ID_FIELD, 0)));
                    break;
                case "$group":
                    Document groupDetails = (Document) stage.get(stageOperation);
                    aggregation.addStage(new GroupStage(groupDetails));
                    break;
                case "$addFields":
                    Document addFieldsDetails = (Document) stage.get(stageOperation);
                    aggregation.addStage(new AddFieldsStage(addFieldsDetails));
                    break;
                case "$unwind":
                    Object unwind = stage.get(stageOperation);
                    aggregation.addStage(new UnwindStage(unwind));
                    break;
                case "$graphLookup":
                    Document graphLookup = (Document) stage.get(stageOperation);
                    aggregation.addStage(new GraphLookupStage(graphLookup, database));
                    break;
                case "$lookup":
                    Document lookup = (Document) stage.get(stageOperation);
                    if (lookup.containsKey(LookupWithPipelineStage.PIPELINE_FIELD)) {
                        aggregation.addStage(new LookupWithPipelineStage(lookup, database, databaseResolver, oplog));
                    } else {
                        aggregation.addStage(new LookupStage(lookup, database));
                    }
                    break;
                case "$replaceRoot":
                    Document replaceRoot = (Document) stage.get(stageOperation);
                    aggregation.addStage(new ReplaceRootStage(replaceRoot));
                    break;
                case "$sample":
                    Object sample = stage.get(stageOperation);
                    aggregation.addStage(new SampleStage(sample));
                    break;
                case "$sortByCount":
                    Object expression = stage.get(stageOperation);
                    aggregation.addStage(new GroupStage(new Document(ID_FIELD, expression).append("count", new Document("$sum", 1))));
                    aggregation.addStage(new SortStage(new Document("count", -1).append(ID_FIELD, 1)));
                    break;
                case "$bucket":
                    Document bucket = (Document) stage.get(stageOperation);
                    aggregation.addStage(new BucketStage(bucket));
                    break;
                case "$facet":
                    Document facet = (Document) stage.get(stageOperation);
                    aggregation.addStage(new FacetStage(facet, databaseResolver, database, collection, oplog));
                    break;
                case "$unset":
                    Object unset = stage.get(stageOperation);
                    aggregation.addStage(new UnsetStage(unset));
                    break;
                case "$indexStats":
                    aggregation.addStage(new IndexStatsStage(collection));
                    break;
                case "$out":
                    Object outCollection = stage.get(stageOperation);
                    aggregation.addStage(new OutStage(database, outCollection));
                    break;
                case "$redact":
                    Document redactExpression = (Document) stage.get(stageOperation);
                    aggregation.addStage(new RedactStage(redactExpression));
                    break;
                case "$merge":
                    Object mergeParams = stage.get(stageOperation);
                    aggregation.addStage(new MergeStage(databaseResolver, database, mergeParams));
                    break;
                case "$geoNear":
                    throw new MongoServerNotYetImplementedException(138, stageOperation);
                default:
                    throw new MongoServerError(40324, "Unrecognized pipeline stage name: '" + stageOperation + "'");
            }
        }
        return aggregation;
    }

    private List<Document> runStages() {
        return runStages(collection.queryAllAsStream());
    }

    public List<Document> runStages(Stream<Document> stream) {
        return runStagesAsStream(stream).collect(Collectors.toList());
    }

    public Stream<Document> runStagesAsStream(Stream<Document> stream) {
        if (hasVariables()) {
            stream = stream.map(this::addAllVariables);
        }
        for (AggregationStage stage : stages) {
            stream = stage.apply(stream);
        }
        if (hasVariables()) {
            stream = stream.map(this::removeAllVariables);
        }
        return stream;
    }

    private boolean hasVariables() {
        return !variables.isEmpty();
    }

    private Document addAllVariables(Document document) {
        Document clone = document.clone();
        clone.putAll(variables);
        return clone;
    }

    private Document removeAllVariables(Document document) {
        return CollectionUtils.removeAll(document, variables.keySet());
    }

    private void addStage(AggregationStage stage) {
        this.stages.add(stage);
    }

    public List<Document> computeResult() {
        if (collection == null) {
            return Collections.emptyList();
        }
        try {
            return runStages();
        } catch (TypeMismatchException | FailedToOptimizePipelineError | ProjectStage.InvalidProjectException e) {
            throw e;
        } catch (MongoServerError e) {
            if (e.hasCode(ErrorCode._15998, ErrorCode._40353)) {
                throw e;
            }
            throw new PlanExecutorError(e);
        }
    }

    public void setVariables(Map<String, Object> variables) {
        this.variables = Collections.unmodifiableMap(variables);
    }

    public void validate(Document query) {
        stages.stream()
            .filter(stage -> stage instanceof TerminalStage)
            .map(TerminalStage.class::cast)
            .filter(terminalStage -> !isLastStage(terminalStage))
            .findFirst()
            .ifPresent(nonFinalTerminalStage -> {
                throw new MongoServerError(40601, nonFinalTerminalStage.name() + " can only be the final stage in the pipeline");
            });

        Document cursor = (Document) query.get("cursor");
        if (cursor == null) {
            throw new FailedToParseException("The 'cursor' option is required, except for aggregate with the explain argument");
        } else if (!cursor.isEmpty()) {
            log.warn("Non-empty cursor is not yet implemented. Ignoring.");
        }
    }

    private boolean isLastStage(TerminalStage stage) {
        Assert.notEmpty(stages);
        return stages.indexOf(stage) == stages.size() - 1;
    }

    public List<AggregationStage> getStages() {
        return stages;
    }

    public boolean isModifying() {
        return stages.stream().anyMatch(AggregationStage::isModifying);
    }

    private static class PlanExecutorError extends MongoServerError {

        private static final long serialVersionUID = 1L;

        private static final String MESSAGE_PREFIX = "PlanExecutor error during aggregation :: caused by :: ";

        private PlanExecutorError(MongoServerError cause) {
            super(cause.getCode(), cause.getCodeName(), MESSAGE_PREFIX + cause.getMessageWithoutErrorCode(),
                cause);
        }
    }
}
<file_sep>/core/src/main/java/de/bwaldvogel/mongo/exception/InvalidOptionsException.java
package de.bwaldvogel.mongo.exception;

public class InvalidOptionsException extends MongoServerError {

    private static final long serialVersionUID = 1L;

    public InvalidOptionsException(String message) {
        super(ErrorCode.InvalidOptions, message);
    }

}
