commit 33bf023a88faff9e7f49d8f30e740115a861090c Author: George Narroway Date: Thu Nov 14 10:01:38 2019 +0800 Initial commit diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..c4c6664 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,28 @@ +version: 2.1 +jobs: + build: + docker: + - image: circleci/clojure:openjdk-11-lein + steps: + - checkout + - run: lein test + +# publish: +# docker: +# - image: circleci/clojure:openjdk-11-lein +# steps: +# - checkout +# - run: lein deploy clojars + + +workflows: + version: 2 + build-publish: + jobs: + - build +# - publish: +# requires: +# - build +# filters: +# branches: +# only: master \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c53038e --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/target +/classes +/checkouts +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port +.hgignore +.hg/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7013821 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 George Simon Narroway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..80e202a --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# mongo-driver-3 + +A Mongo client for clojure, lightly wrapping 3.11+ versions of the [MongoDB Java Driver](https://mongodb.github.io/mongo-java-driver/) + +In general, it will feel familiar to users of mongo clients like [monger](https://github.com/michaelklishin/monger). +Like our HTTP/2 client [hato](https://github.com/gnarroway/hato), the API is designed to be idiomatic and to make common +tasks convenient, whilst still allowing the underlying client to be configured via native Java objects. + +It was developed with the following goals: + +- Up to date with the latest driver versions +- Minimal layer that doesn't block any functionality +- Consistent API across all functions +- Configuration over macros +- Simple + + +## Status + +mongo-driver-3 is under active development but the existing API is unlikely to break. +Please try it out and raise any issues you may find. + +## Usage + +For Leinengen, add this to your project.clj: + +```clojure +;; The underlying driver -- any newer version can also be used +[org.mongodb/mongodb-driver-sync "3.11.0"] + +;; This wrapper library +[com.gnarroway/mongo-driver-3 "0.1.0-SNAPSHOT"] +``` + +## License + +Released under the MIT License: http://www.opensource.org/licenses/mit-license.php diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..61fe65c --- /dev/null +++ b/project.clj @@ -0,0 +1,14 @@ +(defproject mongo-driver-3 "0.1.0-SNAPSHOT" + :description "A Clojure wrapper for the Java MongoDB driver 3.x." + :url "https://github.com/gnarroway/mongo-driver-3" + :license {:name "The MIT License" + :url "http://opensource.org/licenses/mit-license.php" + :distribution :repo} + :deploy-repositories [["clojars" {:url "https://clojars.org/repo" + :username :env/clojars_user + :password :env/clojars_pass + :sign-releases false}]] + :plugins [[lein-cljfmt "0.6.4"]] + + :profiles {:dev {:dependencies [[org.clojure/clojure "1.10.1"] + [org.mongodb/mongodb-driver-sync "3.11.0"]]}}) diff --git a/src/mongo_driver_3/client.clj b/src/mongo_driver_3/client.clj new file mode 100644 index 0000000..90e2826 --- /dev/null +++ b/src/mongo_driver_3/client.clj @@ -0,0 +1,42 @@ +(ns mongo-driver-3.client + (:refer-clojure :exclude [find]) + (:import (com.mongodb.client MongoClients MongoClient) + (com.mongodb ConnectionString))) + +;;; Core + +(defn create + "Creates a connection to a MongoDB + + `connection-string` is a mongo connection string, e.g. mongodb://localhost:27107 + + If a connecting string is not passed in, it will connect to the default localhost instance." + ([] (MongoClients/create)) + ([^String connection-string] + (MongoClients/create connection-string))) + +(defn get-db + "Gets a database by name + + `client` is a MongoClient, e.g. resulting from calling `connect` + `name` is the name of the database to get." + [^MongoClient client ^String name] + (.getDatabase client name)) + +(defn close + "Close a MongoClient and release all resources" + [^MongoClient client] + (.close client)) + +;;; Utility + +(defn connect-to-db + "Connects to a MongoDB database using a URI, returning the client and database as a map with :client and :db. + + This is useful to get a db from a single call, instead of having to create a client and get a db manually." + [connection-string] + (let [uri (ConnectionString. connection-string) + client (MongoClients/create uri)] + (if-let [db-name (.getDatabase uri)] + {:client client :db (.getDatabase client db-name)} + (throw (IllegalArgumentException. "No database name specified in URI. connect-to-db requires database to be explicitly configured."))))) \ No newline at end of file diff --git a/src/mongo_driver_3/collection.clj b/src/mongo_driver_3/collection.clj new file mode 100644 index 0000000..61c0b88 --- /dev/null +++ b/src/mongo_driver_3/collection.clj @@ -0,0 +1,706 @@ +(ns mongo-driver-3.collection + (:refer-clojure :exclude [find empty? drop]) + (:import (clojure.lang Ratio Keyword Named IPersistentMap) + (com.mongodb ReadConcern ReadPreference WriteConcern MongoNamespace) + (com.mongodb.client MongoDatabase MongoCollection TransactionBody MongoCursor) + (com.mongodb.client.model InsertOneOptions InsertManyOptions DeleteOptions FindOneAndUpdateOptions ReturnDocument FindOneAndReplaceOptions CountOptions CreateCollectionOptions RenameCollectionOptions IndexOptions IndexModel UpdateOptions ReplaceOptions) + (java.util List Collection) + (java.util.concurrent TimeUnit) + (org.bson Document) + (org.bson.types Decimal128))) + +;;; Conversions + +(defprotocol ConvertToDocument + (^Document document [input] "Convert some clojure to a Mongo Document")) + +(extend-protocol ConvertToDocument + nil + (document [_] + nil) + + Ratio + (document [^Ratio input] + (double input)) + + Keyword + (document [^Keyword input] + (.getName input)) + + Named + (document [^Named input] + (.getName input)) + + IPersistentMap + (document [^IPersistentMap input] + (let [o (Document.)] + (doseq [[k v] input] + (.append o (document k) (document v))) + o)) + + Collection + (document [^Collection input] + (map document input)) + + Object + (document [input] + input)) + +(defprotocol ConvertFromDocument + (from-document [input keywordize] "Converts given Document to Clojure")) + +(extend-protocol ConvertFromDocument + nil + (from-document [input _] + input) + + Object + (from-document [input _] input) + + Decimal128 + (from-document [^Decimal128 input _] + (.bigDecimalValue input)) + + List + (from-document [^List input keywordize] + (vec (map #(from-document % keywordize) input))) + + Document + (from-document [^Document input keywordize] + (reduce (if keywordize + (fn [m ^String k] + (assoc m (keyword k) (from-document (.get input k) true))) + (fn [m ^String k] + (assoc m k (from-document (.get input k) false)))) + {} (.keySet input)))) + + +;;; Collection + + +(def kw->ReadConcern + {:available (ReadConcern/AVAILABLE) + :default (ReadConcern/DEFAULT) + :linearizable (ReadConcern/LINEARIZABLE) + :local (ReadConcern/LOCAL) + :majority (ReadConcern/MAJORITY) + :snapshot (ReadConcern/SNAPSHOT)}) + +(defn ->ReadConcern + "Coerce `rc` into a ReadConcern if not nil. + + Accepts a ReadConcern or kw corresponding to one: + [:available, :default, :linearizable, :local, :majority, :snapshot] + + Invalid values will throw an exception." + [rc] + (when rc + (if (instance? ReadConcern rc) + rc + (or (kw->ReadConcern rc) (throw (IllegalArgumentException. + (str "No match for read concern of " (name rc)))))))) + +(defn ->ReadPreference + "Coerce `rp` into a ReadPreference if not nil. + + Accepts a ReadPreference or a kw corresponding to one: + [:primary, :primaryPreferred, :secondary, :secondaryPreferred, :nearest] + + Invalid values will throw an exception." + [rp] + (when rp + (if (instance? ReadPreference rp) + rp + (ReadPreference/valueOf (name rp))))) + +(defn ->WriteConcern + "Coerce write-concern related options to a WriteConcern. + + Accepts an options map: + + :write-concern A WriteConcern or kw corresponding to one: + [:acknowledged, :journaled, :majority, :unacknowledged, :w1, :w2, :w3], + defaulting to :acknowledged, if some invalid option is provided. + :write-concern/w an int >= 0, controlling the number of replicas to acknowledge + :write-concern/w-timeout-ms How long to wait for secondaries to acknowledge before failing, + in milliseconds (0 means indefinite). + :write-concern/journal? If true, block until write operations have been committed to the journal." + [{:keys [write-concern write-concern/w write-concern/w-timeout-ms write-concern/journal?]}] + (when (some some? [write-concern w w-timeout-ms journal?]) + (let [wc (when write-concern + (if (instance? WriteConcern write-concern) + write-concern + (WriteConcern/valueOf (name write-concern))))] + (-> (or wc (WriteConcern/ACKNOWLEDGED)) + (#(if (some? w) (.withW % w) %)) + (#(if (some? w-timeout-ms) (.withWTimeout % w-timeout-ms (TimeUnit/MILLISECONDS)) %)) + (#(if (some? journal?) (.withJournal % journal?) %)))))) + +(defn collection + "Coerces `coll` to a MongoCollection with some options. + + `db` is a MongoDatabase + `coll` is a collection name or a MongoCollection. This is to provide flexibility in the use of + higher-level fns (e.g. `find-maps`), either in reuse of instances or in some more complex + configuration we do not directly support. + + Accepts an options map: + :read-preference + :read-concern + :write-concern + :write-concern/w + :write-concern/w-timeout-ms + :write-concern/journal? + + See respective coercion functions for details (->ReadPreference, ->ReadConcern, ->WriteConcern)." + ([^MongoDatabase db coll] + (collection db coll {})) + ([^MongoDatabase db coll opts] + (let [coll' (if (instance? MongoCollection coll) coll (.getCollection db coll)) + {:keys [read-concern read-preference]} opts] + (-> coll' + (#(if-let [rp (->ReadPreference read-preference)] (.withReadPreference % rp) %)) + (#(if-let [rc (->ReadConcern read-concern)] (.withReadConcern % rc) %)) + (#(if-let [wc (->WriteConcern opts)] (.withWriteConcern % wc) %)))))) + +;;; CRUD functions + + +(defn ->CountOptions + "Coerce options map into CountOptions. See `count-documents` for usage." + [{:keys [count-options hint limit max-time-ms skip]}] + (let [opts (or count-options (CountOptions.))] + (when hint (.hint opts (document hint))) + (when limit (.limit opts limit)) + (when max-time-ms (.maxTime opts max-time-ms (TimeUnit/MILLISECONDS))) + (when skip (.skip opts skip)) + + opts)) + +(defn count-documents + "Count documents in a collection, optionally matching a filter query `q`. + + `db` is a MongoDatabase + `coll` is a collection name + `q` is a map representing a query + + Takes an options map: + -- query options + :hint an index name (string) hint or specification (map) + :max-time-ms max amount of time to allow the query to run, in milliseconds + :skip number of documents to skip before counting + :limit max number of documents to count + + :count-options a CountOptions, for configuring directly. If specified, any + other query options will be applied to it. + + -- other options + :session a ClientSession + + Additionally takes options specified in `collection`." + ([^MongoDatabase db coll] + (.countDocuments (collection db coll {}))) + ([^MongoDatabase db coll q] + (count-documents db coll q {})) + ([^MongoDatabase db coll q opts] + (let [opts' (->CountOptions opts)] + (if-let [session (:session opts)] + (.countDocuments (collection db coll opts) session (document q) opts') + (.countDocuments (collection db coll opts) (document q) opts'))))) + +(defn ->DeleteOptions + "Coerce options map into DeleteOptions. See `delete-one` and `delete-many` for usage." + [{:keys [delete-options]}] + (let [opts (or delete-options (DeleteOptions.))] + opts)) + +(defn delete-one + "Deletes a single document from a collection and returns a DeletedResult. + + `db` is a MongoDatabase + `coll` is a collection name + `q` is a map representing a query to match documents to delete. + + Takes an options map: + -- query options + :delete-options a DeleteOptions for configuring directly. + + -- other options + :session A ClientSession + + Additionally takes options specified in `collection`." + ([^MongoDatabase db coll q] + (delete-one db coll q {})) + ([^MongoDatabase db coll q opts] + (if-let [session (:session opts)] + (.deleteOne (collection db coll opts) session (document q) (->DeleteOptions opts)) + (.deleteOne (collection db coll opts) (document q) (->DeleteOptions opts))))) + +(defn delete-many + "Deletes multiple documents from a collection and returns a DeleteResult. + + `db` is a MongoDatabase + `coll` is a collection name + `q` is a map representing a query to match documents to delete. + + Takes an options map: + -- query options + :delete-options A DeleteOptions for configuring directly. + + -- other options + :session A ClientSession + + Additionally takes options specified in `collection`" + ([^MongoDatabase db coll q] + (delete-many db coll q {})) + ([^MongoDatabase db coll q opts] + (if-let [session (:session opts)] + (.deleteMany (collection db coll opts) session (document q) (->DeleteOptions opts)) + (.deleteMany (collection db coll opts) (document q) (->DeleteOptions opts))))) + +(defn find + "Finds documents and returns a FindIterable. + + This is a low level function that returns the result directly from the underling driver. + Use `find-maps` to do some post-processing, e.g. returning a lazy seq + of clojure maps with keyword keys. + + `db` is a MongoDatabase + `coll` is a collection name + `q` is a map representing a query. + + Takes an options map: + :limit Number of results, e.g. 1 + :sort document representing sort order, e.g. {:timestamp -1} + :projection document representing fields to return, e.g. {:_id 0}" + ([^MongoDatabase db coll q] + (find db coll q {})) + ([^MongoDatabase db coll q opts] + (let [{:keys [limit sort projection session]} opts] + (-> (if session + (.find (collection db coll opts) session (document q)) + (.find (collection db coll opts) (document q))) + (#(if limit (.limit % limit) %)) + (#(if sort (.sort % (document sort)) %)) + (#(if projection (.projection % (document projection)) %)))))) + +(defn find-maps + "Finds documents and returns them as a clojure seq of maps. + + Takes the same options as `find`, as well as: + + :keywordize keywordize the keys of returns results, default: true + :session a ClientSession + + Additionally takes options specified in `collection`." + ([^MongoDatabase db coll q] + (find-maps db coll q {})) + ([^MongoDatabase db coll q opts] + (let [{:keys [keywordize] :or {keywordize true}} opts] + (with-open [^MongoCursor iterator (.iterator (find db coll q opts))] + (doall (map (fn [x] (from-document x keywordize)) (iterator-seq iterator))))))) + +(defn find-one-as-map + "Finds a single document and returns it as a clojure map, or nil if not found. + + Takes the same options as `find`, as well as: + + :keywordize keywordize the keys of returns results, default: true + :session a ClientSession + + Additionally takes options specified in `collection`." + ([^MongoDatabase db coll q] + (find-one-as-map db coll q {})) + ([^MongoDatabase db coll q opts] + (let [{:keys [keywordize] :or {keywordize true}} opts] + (-> (find db coll q opts) + (.first) + (from-document keywordize))))) + +(defn ->FindOneAndUpdateOptions + "Coerce options map into FindOneAndUpdateOptions. See `find-one-and-update` for usage." + [{:keys [find-one-and-update-options upsert? return-new? sort projection]}] + (let [opts (or find-one-and-update-options (FindOneAndUpdateOptions.))] + (when (some? upsert?) (.upsert opts upsert?)) + (when return-new? (.returnDocument opts (ReturnDocument/AFTER))) + (when sort (.sort opts (document sort))) + (when projection (.projection opts (document projection))) + + opts)) + +(defn find-one-and-update + "Atomically find a document (at most one) and modify it. + + `db` is a MongoDatabase + `coll` is a collection name + `q` is a map representing a query to find the document to update + `update` is a map representing an update. The update to apply must include only update operators. + + Takes an options map: + -- query options + :upsert? whether to insert a new document if nothing is found, default: false + :return-new? whether to return the document after update (insead of its state before the update), default: false + :sort map representing sort order, e.g. {:timestamp -1} + :projection map representing fields to return, e.g. {:_id 0} + + :find-one-and-update-options A FindOneAndUpdateOptions for configuring directly. If specified, + any other query options will be applied to it. + + -- other options + :keywordize keywordize the keys of returns results, default: true + :session a ClientSession + + Additionally takes options specified in `collection`" + ([^MongoDatabase db coll q update] + (find-one-and-update db coll q update {})) + ([^MongoDatabase db coll q update opts] + (let [{:keys [keywordize session] :or {keywordize true}} opts + opts' (->FindOneAndUpdateOptions opts)] + (-> (if session + (.findOneAndUpdate (collection db coll opts) session (document q) (document update) opts') + (.findOneAndUpdate (collection db coll opts) (document q) (document update) opts')) + (from-document keywordize))))) + +(defn ->FindOneAndReplaceOptions + "Coerce options map into FindOneAndReplaceOptions. See `find-one-and-replace` for usage." + [{:keys [find-one-and-replace-options upsert? return-new? sort projection]}] + (let [opts (or find-one-and-replace-options (FindOneAndReplaceOptions.))] + (when (some? upsert?) (.upsert opts upsert?)) + (when return-new? (.returnDocument opts (ReturnDocument/AFTER))) + (when sort (.sort opts (document sort))) + (when projection (.projection opts (document projection))) + + opts)) + +(defn find-one-and-replace + "Atomically find a document (at most one) and replace it. + + `db` is a MongoDatabase + `coll` is a collection name + `q` is a map representing a query to find the document to update + `doc` is a new document to add. + + Takes an options map: + -- query options + :upsert? whether to insert a new document if nothing is found, default: false + :return-new? whether to return the document after update (insead of its state before the update), default: false + :sort map representing sort order, e.g. {:timestamp -1} + :projection map representing fields to return, e.g. {:_id 0} + + :find-one-and-replace-options A FindOneAndReplaceOptions for configuring directly. If specified, + any other query options will be applied to it. + + -- other options + :keywordize keywordize the keys of returns results, default: true + :session a ClientSession + + Additionally takes options specified in `collection`" + ([^MongoDatabase db coll q doc] + (find-one-and-replace db coll q doc {})) + ([^MongoDatabase db coll q doc opts] + (let [{:keys [keywordize session] :or {keywordize true}} opts + opts' (->FindOneAndReplaceOptions opts)] + (-> (if session + (.findOneAndReplace (collection db coll opts) session (document q) (document doc) opts') + (.findOneAndReplace (collection db coll opts) (document q) (document doc) opts')) + (from-document keywordize))))) + +(defn ->InsertOneOptions + "Coerce options map into InsertOneOptions. See `insert-one` for usage." + [{:keys [insert-one-options bypass-document-validation?]}] + (let [opts (or insert-one-options (InsertOneOptions.))] + (when (some? bypass-document-validation?) (.bypassDocumentValidation opts bypass-document-validation?)) + + opts)) + +(defn insert-one + "Inserts a single document into a collection. + If the document does not have an _id field, it will be auto-generated by the underlying driver. + + `db` is a MongoDatabase + `coll` is a collection name + `doc` is a map to insert + + Takes an options map: + -- query options + :bypass-document-validation? Boolean + + :insert-one-options An InsertOneOptions for configuring directly. If specified, + any other query options will be applied to it. + + -- other options + :session A ClientSession + + Additionally takes options specified in `collection`." + ([^MongoDatabase db coll doc] + (insert-one db coll doc {})) + ([^MongoDatabase db coll doc opts] + (let [opts' (->InsertOneOptions opts)] + (if-let [session (:session opts)] + (.insertOne (collection db coll opts) session (document doc) opts') + (.insertOne (collection db coll opts) (document doc) opts'))))) + +(defn ->InsertManyOptions + "Coerce options map into InsertManyOptions. See `insert-many` for usage." + [{:keys [insert-many-options bypass-document-validation? ordered?]}] + (let [opts (or insert-many-options (InsertManyOptions.))] + (when (some? bypass-document-validation?) (.bypassDocumentValidation opts bypass-document-validation?)) + (when (some? ordered?) (.ordered opts ordered?)) + + opts)) + +(defn insert-many + "Inserts multiple documents into a collection. + If a document does not have an _id field, it will be auto-generated by the underlying driver. + + `db` is a MongoDatabase + `coll` is a collection name + `docs` is a collection of maps to insert + + Takes an options map: + -- query options + :bypass-document-valiation? Boolean + :ordered? Boolean whether serve should insert documents in order provided (default true) + + :insert-many-options An InsertManyOptions for configuring directly. If specified, + any other query options will be applied to it. + + -- other options + :session A ClientSession + + Additionally takes options specified in `collection`" + ([^MongoDatabase db coll docs] + (insert-many db coll docs {})) + ([^MongoDatabase db coll docs opts] + (let [opts' (->InsertManyOptions opts)] + (if-let [session (:session opts)] + (.insertMany (collection db coll opts) session (map document docs) opts') + (.insertMany (collection db coll opts) (map document docs) opts'))))) + +(defn ->ReplaceOptions + "Coerce options map into ReplaceOptions. See `replace-one` and `replace-many` for usage." + [{:keys [replace-options upsert? bypass-document-validation?]}] + (let [opts (or replace-options (ReplaceOptions.))] + (when (some? upsert?) (.upsert opts upsert?)) + (when (some? bypass-document-validation?) (.bypassDocumentValidation opts bypass-document-validation?)) + + opts)) + +(defn replace-one + "Replace a single document. + + `db` is a MongoDatabase + `coll` is a collection name + `q` is a map representing a query to find the document to update + `doc` is a new document to add. + + Takes an options map: + -- query options + :upsert? whether to insert a new document if nothing is found, default: false + :bypass-document-validation? + + :eplace-options A ReplaceOptions for configuring directly. If specified, + any other query options will be applied to it. + + -- other options + :session a ClientSession + + Additionally takes options specified in `collection`" + ([^MongoDatabase db coll q doc] + (find-one-and-replace db coll q doc {})) + ([^MongoDatabase db coll q doc opts] + (if-let [session (:session opts)] + (.replaceOne (collection db coll opts) session (document q) (document doc) (->ReplaceOptions opts)) + (.replaceOne (collection db coll opts) (document q) (document doc) (->ReplaceOptions opts))))) + +(defn ->UpdateOptions + "Coerce options map into UpdateOptions. See `updatee-one` and `update-many` for usage." + [{:keys [update-options upsert? bypass-document-validation?]}] + (let [opts (or update-options (UpdateOptions.))] + (when (some? upsert?) (.upsert opts upsert?)) + (when (some? bypass-document-validation?) (.bypassDocumentValidation opts bypass-document-validation?)) + + opts)) + +(defn update-one + "Updates a single document from a collection and returns a UpdateResult. + + `db` is a MongoDatabase + `coll` is a collection name + `q` is a map representing a query to match documents to update. + `update` is a map representing an update. The update to apply must include only update operators. + + Takes an options map: + -- query options + :upsert? Boolean whether to insert a new document if there are no matches to the query + :bypass-document-validation? Boolean + + :update-options an UpdateOptions for configuring directly. If specified, + any other query options will be applied to it. + + -- other options + :session A ClientSession + + Additionally takes options specified in `collection`." + ([^MongoDatabase db coll q update] + (update-one db coll q update {})) + ([^MongoDatabase db coll q update opts] + (if-let [session (:session opts)] + (.updateOne (collection db coll opts) session (document q) (document update) (->UpdateOptions opts)) + (.updateOne (collection db coll opts) (document q) (document update) (->UpdateOptions opts))))) + +(defn update-many + "Updates many documents from a collection and returns a UpdateResult. + + `db` is a MongoDatabase + `coll` is a collection name + `q` is a map representing a query to match documents to update. + `update` is a map representing an update. The update to apply must include only update operators. + + Takes an options map: + -- query options + :upsert? Boolean whether to insert a new document if there are no matches to the query + :bypass-document-validation? Boolean + + :update-options an UpdateOptions for configuring directly. If specified, + any other query options will be applied to it. + + -- other options + :session A ClientSession + + Additionally takes options specified in `collection`." + ([^MongoDatabase db coll q update] + (update-many db coll q {})) + ([^MongoDatabase db coll q update opts] + (if-let [session (:session opts)] + (.updateMany (collection db coll opts) session (document q) (document update) (->UpdateOptions opts)) + (.updateMany (collection db coll opts) (document q) (document update) (->UpdateOptions opts))))) + +;;; Admin functions + +(defn ->CreateCollectionOptions + "Coerce options map into CreateCollectionOptions. See `create` usage." + [{:keys [create-collection-options capped? max-documents max-size-bytes]}] + (let [opts (or create-collection-options (CreateCollectionOptions.))] + (when (some? capped?) (.capped opts capped?)) + (when max-documents (.maxDocuments opts max-documents)) + (when max-size-bytes (.sizeInBytes opts max-size-bytes)) + + opts)) + +(defn create + "Creates a collection + + Takes an options map: + -- query options + :capped? Boolean whether to create a capped collection + :max-documents max documents for a capped collection + :max-size-bytes max collection size in bytes for a capped collection + + :create-collection-options A CreateCollectionOptions for configuring directly. If specified, + any other query options will be applied to it" + ([^MongoDatabase db coll] + (create db coll {})) + ([^MongoDatabase db coll opts] + (let [opts' (->CreateCollectionOptions opts)] + (.createCollection db coll opts')))) + +(defn rename + "Renames `coll` to `new-coll` in the same DB." + [^MongoDatabase db coll new-coll opts] + (let [{:keys [drop-target?]} opts + opts' (RenameCollectionOptions.)] + + (when drop-target? (.dropTarget opts' true)) + + (.renameCollection (collection db coll opts) + (MongoNamespace. (.getName db) new-coll) + opts'))) + +(defn drop + "Drops a collection from a database." + [^MongoDatabase db coll] + (.drop (collection db coll))) + +(defn ->IndexOptions + "Coerces an options map into an IndexOptions. + + See `create-index` for usage" + [{:keys [index-options name sparse? unique?]}] + (let [opts (or index-options (IndexOptions.))] + (when name (.name opts name)) + (when (some? sparse?) (.sparse opts sparse?)) + (when (some? unique?) (.unique opts unique?)) + + opts)) + +(defn create-index + "Creates an index + + `db` is a MongoDatabase + `coll` is a collection name + `keys` is a document representing index keys, e.g. {:a 1} + + Takes an options map: + -- query options + :name + :sparse? + :unique? + + :index-options An IndexOptions for configuring directly. If specified, + any other query options will be applied to it" + ([^MongoDatabase db coll keys] + (create-index db coll keys {})) + ([^MongoDatabase db coll keys opts] + (.createIndex (collection db coll opts) (document keys) (->IndexOptions opts)))) + +(defn create-indexes + "Creates many indexes. + + `db` is a MongoDatabase + `coll` is a collection name + `indexes` is a collection of maps with the following attributes: + + -- required + :keys a document representing index keys, e.g. {:a 1} + + -- optional + any attributes available in `->IndexOptions`" + [^MongoDatabase db coll indexes] + (->> indexes + (map (fn [x] (IndexModel. (document (:keys x)) (->IndexOptions x)))) + (.createIndexes (collection db coll)))) + +(defn list-indexes + "Lists indexes." + [^MongoDatabase db coll] + (->> (.listIndexes (collection db coll)) + (map #(from-document % true)))) + +;;; Utility functions + +(defn empty? + "Returns true if a collection is empty" + [^MongoDatabase db coll] + (zero? (count-documents db coll))) + +(defn exists? + "Returns true if collection exists" + [^MongoDatabase db coll] + (some #(= coll %) (.listCollectionNames db))) + +(defn with-transaction + "Executes `body` in a transaction. + + `body` should be a fn with one or more mongo operations in it. + Ensure `session` is passed as an option to each operation. + + e.g. + (def s (.startSession conn)) + (with-transaction s + (fn [] + (insert-one my-db \"coll\" {:name \"hello\"} {:session s}) + (insert-one my-db \"coll\" {:name \"world\"} {:session s})))" + [session body] + (.withTransaction session (reify TransactionBody + (execute [_] body)))) \ No newline at end of file diff --git a/test/mongo_driver_3/client_test.clj b/test/mongo_driver_3/client_test.clj new file mode 100644 index 0000000..96a4049 --- /dev/null +++ b/test/mongo_driver_3/client_test.clj @@ -0,0 +1,21 @@ +(ns mongo-driver-3.client-test + (:require [clojure.test :refer :all] + [mongo-driver-3.client :as mg]) + (:import (com.mongodb.client MongoClient MongoDatabase))) + +;;; Integration + +; docker run -it --rm -p 27017:27017 mongo:4.2 + +(def mongo-host (or (System/getenv "MONGO_HOST") "mongodb://localhost:27017")) + +(deftest test-create + (is (instance? MongoClient (mg/create))) + (is (instance? MongoClient (mg/create mongo-host)))) + +(deftest test-connect-to-db + (is (thrown? IllegalArgumentException (mg/connect-to-db mongo-host))) + (let [res (mg/connect-to-db (str mongo-host "/my-db"))] + (is (instance? MongoClient (:client res))) + (is (instance? MongoDatabase (:db res))) + (is (= "my-db" (.getName (:db res)))))) \ No newline at end of file diff --git a/test/mongo_driver_3/collection_test.clj b/test/mongo_driver_3/collection_test.clj new file mode 100644 index 0000000..3f3abb9 --- /dev/null +++ b/test/mongo_driver_3/collection_test.clj @@ -0,0 +1,462 @@ +(ns mongo-driver-3.collection-test + (:require [clojure.test :refer :all] + [mongo-driver-3.client :as mg] + [mongo-driver-3.collection :as mc]) + (:import (com.mongodb ReadConcern ReadPreference WriteConcern) + (java.util.concurrent TimeUnit) + (com.mongodb.client.model InsertOneOptions InsertManyOptions DeleteOptions FindOneAndUpdateOptions ReturnDocument FindOneAndReplaceOptions CountOptions UpdateOptions ReplaceOptions IndexOptions CreateCollectionOptions) + (java.time ZoneId LocalDate LocalTime LocalDateTime) + (java.util Date UUID))) + +;;; Unit + +(deftest test->ReadConcern + (is (nil? (mc/->ReadConcern nil))) + (is (thrown? IllegalArgumentException (mc/->ReadConcern "invalid"))) + (is (instance? ReadConcern (mc/->ReadConcern :available)))) + +(deftest test->ReadPreference + (is (nil? (mc/->ReadPreference nil))) + (is (thrown? IllegalArgumentException (mc/->ReadPreference "invalid"))) + (is (instance? ReadPreference (mc/->ReadPreference :primary)))) + +(deftest test->WriteConcern + (is (= (WriteConcern/W1) (mc/->WriteConcern {:write-concern :w1})) "accepts kw") + (is (= (WriteConcern/W1) (mc/->WriteConcern {:write-concern (WriteConcern/W1)})) "accepts WriteConcern") + (is (= (WriteConcern/ACKNOWLEDGED) (mc/->WriteConcern {:write-concern "invalid"})) "defaults to acknowledged") + (is (= 1 (.getW (mc/->WriteConcern {:write-concern/w 1}))) "set w") + (is (= 2 (.getW (mc/->WriteConcern {:write-concern (WriteConcern/W2)})))) + (is (= 1 (.getW (mc/->WriteConcern {:write-concern (WriteConcern/W2) :write-concern/w 1}))) "prefer granular option") + (is (true? (.getJournal (mc/->WriteConcern {:write-concern/journal? true}))) "can set journal") + (is (= 77 (.getWTimeout (mc/->WriteConcern {:write-concern/w-timeout-ms 77}) (TimeUnit/MILLISECONDS))) "can set timeout")) + +(deftest test->InsertOneOptions + (is (instance? InsertOneOptions (mc/->InsertOneOptions {}))) + (is (true? (.getBypassDocumentValidation (mc/->InsertOneOptions {:bypass-document-validation? true})))) + (is (true? (.getBypassDocumentValidation (mc/->InsertOneOptions + {:insert-one-options (.bypassDocumentValidation (InsertOneOptions.) true)}))) + "configure directly") + (is (false? (.getBypassDocumentValidation (mc/->InsertOneOptions + {:insert-one-options (.bypassDocumentValidation (InsertOneOptions.) true) + :bypass-document-validation? false}))) + "can override")) + +(deftest test->ReplaceOptions + (is (instance? ReplaceOptions (mc/->ReplaceOptions {}))) + (are [expected arg] + (= expected (.isUpsert (mc/->ReplaceOptions {:upsert? arg}))) + true true + false false + false nil) (is (true? (.getBypassDocumentValidation (mc/->ReplaceOptions {:bypass-document-validation? true})))) + (is (true? (.getBypassDocumentValidation (mc/->ReplaceOptions + {:replace-options (.bypassDocumentValidation (ReplaceOptions.) true)}))) + "configure directly") + (is (false? (.getBypassDocumentValidation (mc/->ReplaceOptions + {:replace-options (.bypassDocumentValidation (ReplaceOptions.) true) + :bypass-document-validation? false}))) + "can override")) + +(deftest test->UpdateOptions + (is (instance? UpdateOptions (mc/->UpdateOptions {}))) + (are [expected arg] + (= expected (.isUpsert (mc/->UpdateOptions {:upsert? arg}))) + true true + false false + false nil) + (is (true? (.getBypassDocumentValidation (mc/->UpdateOptions {:bypass-document-validation? true})))) + (is (true? (.getBypassDocumentValidation (mc/->UpdateOptions + {:update-options (.bypassDocumentValidation (UpdateOptions.) true)}))) + "configure directly") + (is (false? (.getBypassDocumentValidation (mc/->UpdateOptions + {:update-options (.bypassDocumentValidation (UpdateOptions.) true) + :bypass-document-validation? false}))) + "can override")) + +(deftest test->InsertManyOptions + (is (instance? InsertManyOptions (mc/->InsertManyOptions {}))) + (are [expected arg] + (= expected (.getBypassDocumentValidation (mc/->InsertManyOptions {:bypass-document-validation? arg}))) + true true + false false + nil nil) + (are [expected arg] + (= expected (.isOrdered (mc/->InsertManyOptions {:ordered? arg}))) + true true + false false + true nil) + (is (true? (.getBypassDocumentValidation (mc/->InsertManyOptions + {:insert-many-options (.bypassDocumentValidation (InsertManyOptions.) true)}))) + "configure directly") + (is (false? (.getBypassDocumentValidation (mc/->InsertManyOptions + {:insert-one-options (.bypassDocumentValidation (InsertManyOptions.) true) + :bypass-document-validation? false}))) + "can override")) + +(deftest test->DeleteOptions + (is (instance? DeleteOptions (mc/->DeleteOptions {}))) + (let [opts (DeleteOptions.)] + (is (= opts (mc/->DeleteOptions {:delete-options opts})) "configure directly"))) + +(deftest test->FindOneAndUpdateOptions + (is (instance? FindOneAndUpdateOptions (mc/->FindOneAndUpdateOptions {}))) + (let [opts (FindOneAndUpdateOptions.)] + (is (= opts (mc/->FindOneAndUpdateOptions {:find-one-and-update-options opts})) "configure directly")) + (are [expected arg] + (= expected (.isUpsert (mc/->FindOneAndUpdateOptions {:upsert? arg}))) + true true + false false + false nil) + (are [expected arg] + (= expected (.getReturnDocument (mc/->FindOneAndUpdateOptions {:return-new? arg}))) + (ReturnDocument/AFTER) true + (ReturnDocument/BEFORE) false + (ReturnDocument/BEFORE) nil) + + (is (= {"_id" 1} (.getSort (mc/->FindOneAndUpdateOptions {:sort {:_id 1}})))) + (is (= {"_id" 1} (.getProjection (mc/->FindOneAndUpdateOptions {:projection {:_id 1}}))))) + +(deftest test->FindOneAndReplaceOptions + (is (instance? FindOneAndReplaceOptions (mc/->FindOneAndReplaceOptions {}))) + (let [opts (FindOneAndReplaceOptions.)] + (is (= opts (mc/->FindOneAndReplaceOptions {:find-one-and-replace-options opts})) "configure directly")) + (are [expected arg] + (= expected (.isUpsert (mc/->FindOneAndReplaceOptions {:upsert? arg}))) + true true + false false + false nil) + (are [expected arg] + (= expected (.getReturnDocument (mc/->FindOneAndReplaceOptions {:return-new? arg}))) + (ReturnDocument/AFTER) true + (ReturnDocument/BEFORE) false + (ReturnDocument/BEFORE) nil) + + (is (= {"_id" 1} (.getSort (mc/->FindOneAndReplaceOptions {:sort {:_id 1}})))) + (is (= {"_id" 1} (.getProjection (mc/->FindOneAndReplaceOptions {:projection {:_id 1}}))))) + +(deftest test->CountOptions + (is (instance? CountOptions (mc/->CountOptions {}))) + (let [opts (CountOptions.)] + (is (= opts (mc/->CountOptions {:count-options opts})) "configure directly")) + (is (= {"a" 1} (.getHint (mc/->CountOptions {:hint {:a 1}})))) + (is (= 7 (.getLimit (mc/->CountOptions {:limit 7})))) + (is (= 2 (.getSkip (mc/->CountOptions {:skip 2})))) + (is (= 42 (.getMaxTime (mc/->CountOptions {:max-time-ms 42}) (TimeUnit/MILLISECONDS))))) + +(deftest test->IndexOptions + (is (instance? IndexOptions (mc/->IndexOptions {}))) + (are [expected arg] + (= expected (.isSparse (mc/->IndexOptions {:sparse? arg}))) + true true + false false + false nil) + (are [expected arg] + (= expected (.isUnique (mc/->IndexOptions {:unique? arg}))) + true true + false false + false nil) + (let [opts (IndexOptions.)] + (is (= opts (mc/->IndexOptions {:index-options opts})) "configure directly"))) + +(deftest test->CreateCollectionOptions + (are [expected arg] + (= expected (.isCapped (mc/->CreateCollectionOptions {:capped? arg}))) + true true + false false + false nil) + (is (= 7 (.getMaxDocuments (mc/->CreateCollectionOptions {:max-documents 7})))) + (is (= 42 (.getSizeInBytes (mc/->CreateCollectionOptions {:max-size-bytes 42})))) + (let [opts (-> (CreateCollectionOptions.) (.maxDocuments 5))] + (is (= opts (mc/->CreateCollectionOptions {:create-collection-options opts})) "configure directly") + (is (= 5 (.getMaxDocuments (mc/->CreateCollectionOptions {:create-collection-options opts})))) + (is (= 7 (.getMaxDocuments (mc/->CreateCollectionOptions {:create-collection-options opts :max-documents 7}))) + "can override"))) + +;;; Integration + +; docker run -it --rm -p 27017:27017 mongo:4.2 + +(def client (atom nil)) + +(defn- setup-connections [f] + (reset! client (mg/create)) + (f) + (mg/close @client)) + +(use-fixtures :once setup-connections) + +(defn new-db + [client] + (mg/get-db client (.toString (UUID/randomUUID)))) + +(deftest ^:integration test-insert-one + (testing "basic insertion" + (let [db (new-db @client) + doc {:hello "world"} + _ (mc/insert-one db "test" doc) + res (mc/find-maps db "test" {})] + (is (= 1 (count res))) + (is (= doc (select-keys (first res) [:hello]))))) + + (testing "conversion round trip" + (let [db (new-db @client) + doc {:nil nil + :string "string" + :int 1 + :float 1.1 + :ratio 1/2 + :list ["moo" "cow"] + :set #{1 2} + :map {:moo "cow"} + :kw :keyword + :bool true + :date (Date.) + :localdate (LocalDate/now) + :localdatetime (LocalDateTime/now) + :localtime (LocalTime/now)} + _ (mc/insert-one db "test" doc) + res (mc/find-one-as-map db "test" {} {:projection {:_id 0}})] + (is (= {:nil nil + :string "string" + :int 1 + :float 1.1 + :ratio 0.5 + :list ["moo" "cow"] + :set [1 2] + :map {:moo "cow"} + :kw "keyword" + :bool true + :date (:date doc) + :localdate (Date/from (.toInstant (.atStartOfDay (:localdate doc) (ZoneId/of "UTC")))) + :localdatetime (Date/from (.toInstant (.atZone (:localdatetime doc) (ZoneId/of "UTC")))) + :localtime (Date/from (.toInstant (.atZone (LocalDateTime/of (LocalDate/EPOCH) (:localtime doc)) (ZoneId/of "UTC"))))} res))))) + +(deftest ^:integration test-insert-many + (testing "basic insertions" + (let [db (new-db @client) + _ (mc/insert-many db "test" [{:id 1} {:id 2}]) + res (mc/find-maps db "test" {})] + (is (= 2 (count res))) + (is (= [1 2] (map :id res))))) + + (testing "no docs" + (let [db (new-db @client)] + (is (thrown? IllegalArgumentException (mc/insert-many db "test" []))) + (is (thrown? IllegalArgumentException (mc/insert-many db "test" nil)))))) + +(deftest ^:integration test-delete-one + (testing "not exist" + (let [db (new-db @client) + res (mc/delete-one db "test" {:id :a})] + (is (= 0 (.getDeletedCount res))))) + + (testing "one at a time" + (let [db (new-db @client) + _ (mc/insert-many db "test" [{:v 1} {:v 1}])] + (is (= 1 (.getDeletedCount (mc/delete-one db "test" {:v 1})))) + (is (= 1 (.getDeletedCount (mc/delete-one db "test" {:v 1})))) + (is (= 0 (.getDeletedCount (mc/delete-one db "test" {:v 1}))))))) + +(deftest ^:integration test-delete-many + (testing "not exist" + (let [db (new-db @client) + res (mc/delete-many db "test" {:id :a})] + (is (= 0 (.getDeletedCount res))))) + + (testing "all together" + (let [db (new-db @client) + _ (mc/insert-many db "test" [{:v 1} {:v 1}])] + (is (= 2 (.getDeletedCount (mc/delete-many db "test" {:v 1})))) + (is (= 0 (.getDeletedCount (mc/delete-many db "test" {:v 1}))))))) + +(deftest ^:integration test-find-maps + (let [db (new-db @client) + _ (mc/insert-many db "test" [{:id 1 :a 1 :v 2} {:id 2 :a 1 :v 3} {:id 3 :v 1}])] + + (testing "query" + (are [ids q] (= ids (map :id (mc/find-maps db "test" q))) + [1 2 3] {} + [1] {:id 1} + [1 2] {:a {:$exists true}} + [2] {:v {:$gt 2}})) + + (testing "sort" + (are [ids s] (= ids (map :id (mc/find-maps db "test" {} {:sort s}))) + [1 2 3] {} + [3 1 2] {:v 1} + [2 1 3] {:v -1})) + + (testing "limit" + (are [cnt n] (= cnt (count (mc/find-maps db "test" {} {:limit n}))) + 1 1 + 2 2 + 3 3 + 3 4)) + + (testing "projection" + (are [ks p] (= ks (keys (first (mc/find-maps db "test" {} {:projection p})))) + [:_id :id :a :v] {} + [:_id :a] {:a 1} + [:id :a :v] {:_id 0})))) + +(deftest ^:integration test-find-one-as-map + (let [db (new-db @client) + _ (mc/insert-many db "test" [{:id 1 :a 1 :v 2} {:id 2 :a 1 :v 3} {:id 3 :v 1}])] + + (testing "query" + (are [id q] (= id (:id (mc/find-one-as-map db "test" q))) + 1 {} + 2 {:id 2} + 1 {:a {:$exists true}} + 2 {:v {:$gt 2}})) + + (testing "sort" + (are [id s] (= id (:id (mc/find-one-as-map db "test" {} {:sort s}))) + 1 {} + 3 {:v 1} + 2 {:v -1})) + + (testing "projection" + (are [ks p] (= ks (keys (mc/find-one-as-map db "test" {} {:projection p}))) + [:_id :id :a :v] {} + [:_id :a] {:a 1} + [:id :a :v] {:_id 0})))) + +(deftest ^:integration test-count-documents + (let [db (new-db @client) + _ (mc/insert-many db "test" [{:id 1 :a 1 :v 2} {:id 2 :a 1 :v 3} {:id 3 :v 1}])] + + (testing "all" + (is (= 3 (mc/count-documents db "test")))) + + (testing "query" + (are [id q] (= id (mc/count-documents db "test" q)) + 3 {} + 1 {:id 2} + 2 {:a {:$exists true}} + 1 {:v {:$gt 2}})) + + (testing "skip" + (are [id s] (= id (mc/count-documents db "test" {} {:skip s})) + 3 nil + 3 0 + 2 1 + 1 2 + 0 3 + 0 4)) + + (testing "limit" + (are [id s] (= id (mc/count-documents db "test" {} {:limit s})) + 3 nil + 3 0 + 1 1 + 2 2 + 3 3 + 3 4)))) + +(deftest ^:integration test-find-one-and-update + (testing "return new" + (let [db (new-db @client) + _ (mc/insert-many db "test" [{:id 1 :v 1} {:id 2 :v 1}])] + + (is (= {:id 1 :v 1} (dissoc (mc/find-one-and-update db "test" {:id 1} {:$set {:r 1} :$inc {:v 1}}) :_id))) + (is (= {:id 2 :v 2 :r 1} (dissoc (mc/find-one-and-update db "test" {:id 2} {:$set {:r 1} :$inc {:v 1}} {:return-new? true}) :_id))))) + + (testing "upsert" + (let [db (new-db @client)] + + (is (nil? (dissoc (mc/find-one-and-update db "test" {:id 1} {:$set {:r 1}} {:return-new? true}) :_id))) + (is (= {:id 1 :r 1} (dissoc (mc/find-one-and-update db "test" {:id 1} {:$set {:r 1}} {:return-new? true :upsert? true}) :_id)))))) + +(deftest ^:integration test-find-one-and-replace + (testing "return new" + (let [db (new-db @client) + _ (mc/insert-many db "test" [{:id 1 :v 1} {:id 2 :v 1}])] + + (is (= {:id 1 :v 1} (dissoc (mc/find-one-and-replace db "test" {:id 1} {:id 1 :v 2}) :_id))) + (is (= {:id 2 :v 2} (dissoc (mc/find-one-and-replace db "test" {:id 2} {:id 2 :v 2} {:return-new? true}) :_id))))) + + (testing "upsert" + (let [db (new-db @client)] + + (is (nil? (dissoc (mc/find-one-and-replace db "test" {:id 1} {:id 1 :v 2} {:return-new? true}) :_id))) + (is (= {:id 1 :v 2} (dissoc (mc/find-one-and-replace db "test" {:id 1} {:id 1 :v 2} {:return-new? true :upsert? true}) :_id)))))) + +(deftest ^:integration test-replace-one + (testing "existing doc" + (let [db (new-db @client) + _ (mc/insert-many db "test" [{:id 1 :v 1} {:id 2 :v 1}]) + r1 (mc/replace-one db "test" {:v 1} {:v 2} {}) + r2 (mc/replace-one db "test" {:v 1} {:v 2} {}) + r3 (mc/replace-one db "test" {:v 1} {:v 2} {})] + ;; replace-one will match at most 1 + (is (= 1 (.getMatchedCount r1))) + (is (= 1 (.getModifiedCount r1))) + (is (= 1 (.getMatchedCount r2))) + (is (= 1 (.getModifiedCount r2))) + (is (= 0 (.getMatchedCount r3))) + (is (= 0 (.getModifiedCount r3))))) + + (testing "upsert" + (let [db (new-db @client)] + + (let [res (mc/replace-one db "test" {:id 1} {:v 2} {})] + (is (= 0 (.getMatchedCount res))) + (is (= 0 (.getModifiedCount res))) + (is (nil? (.getUpsertedId res)))) + + (let [res (mc/replace-one db "test" {:id 1} {:v 2} {:upsert? true})] + (is (= 0 (.getMatchedCount res))) + (is (= 0 (.getModifiedCount res))) + (is (some? (.getUpsertedId res))))))) + +(deftest ^:integration test-update-one + (testing "existing doc" + (let [db (new-db @client) + _ (mc/insert-many db "test" [{:id 1 :v 1} {:id 2 :v 1}]) + r1 (mc/update-one db "test" {:v 1} {:$set {:v 2}} {}) + r2 (mc/update-one db "test" {:v 1} {:$set {:v 2}} {}) + r3 (mc/update-one db "test" {:v 1} {:$set {:v 2}} {})] + ;; update-one will match at most 1 + (is (= 1 (.getMatchedCount r1))) + (is (= 1 (.getModifiedCount r1))) + (is (= 1 (.getMatchedCount r2))) + (is (= 1 (.getModifiedCount r2))) + (is (= 0 (.getMatchedCount r3))) + (is (= 0 (.getModifiedCount r3))))) + + (testing "upsert" + (let [db (new-db @client)] + + (let [res (mc/update-one db "test" {:id 1} {:$set {:r 1}} {})] + (is (= 0 (.getMatchedCount res))) + (is (= 0 (.getModifiedCount res))) + (is (nil? (.getUpsertedId res)))) + + (let [res (mc/update-one db "test" {:id 1} {:$set {:r 1}} {:upsert? true})] + (is (= 0 (.getMatchedCount res))) + (is (= 0 (.getModifiedCount res))) + (is (some? (.getUpsertedId res))))))) + +(deftest ^:integration test-update-many + (testing "existing doc" + (let [db (new-db @client) + _ (mc/insert-many db "test" [{:id 1 :v 1} {:id 2 :v 1}]) + r1 (mc/update-many db "test" {:v 1} {:$set {:v 2}} {}) + r2 (mc/update-many db "test" {:v 1} {:$set {:v 2}} {})] + (is (= 2 (.getMatchedCount r1))) + (is (= 2 (.getModifiedCount r1))) + (is (= 0 (.getMatchedCount r2))) + (is (= 0 (.getModifiedCount r2))))) + + (testing "upsert" + (let [db (new-db @client)] + + (let [res (mc/update-many db "test" {:id 1} {:$set {:r 1}} {})] + (is (= 0 (.getMatchedCount res))) + (is (= 0 (.getModifiedCount res))) + (is (nil? (.getUpsertedId res)))) + + (let [res (mc/update-many db "test" {:id 1} {:$set {:r 1}} {:upsert? true})] + (is (= 0 (.getMatchedCount res))) + (is (= 0 (.getModifiedCount res))) + (is (some? (.getUpsertedId res))))))) \ No newline at end of file