Initial commit
This commit is contained in:
commit
33bf023a88
9 changed files with 1342 additions and 0 deletions
28
.circleci/config.yml
Normal file
28
.circleci/config.yml
Normal file
|
|
@ -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
|
||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/target
|
||||
/classes
|
||||
/checkouts
|
||||
pom.xml
|
||||
pom.xml.asc
|
||||
*.jar
|
||||
*.class
|
||||
/.lein-*
|
||||
/.nrepl-port
|
||||
.hgignore
|
||||
.hg/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -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.
|
||||
37
README.md
Normal file
37
README.md
Normal file
|
|
@ -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
|
||||
14
project.clj
Normal file
14
project.clj
Normal file
|
|
@ -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"]]}})
|
||||
42
src/mongo_driver_3/client.clj
Normal file
42
src/mongo_driver_3/client.clj
Normal file
|
|
@ -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.")))))
|
||||
706
src/mongo_driver_3/collection.clj
Normal file
706
src/mongo_driver_3/collection.clj
Normal file
|
|
@ -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))))
|
||||
21
test/mongo_driver_3/client_test.clj
Normal file
21
test/mongo_driver_3/client_test.clj
Normal file
|
|
@ -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))))))
|
||||
462
test/mongo_driver_3/collection_test.clj
Normal file
462
test/mongo_driver_3/collection_test.clj
Normal file
|
|
@ -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)))))))
|
||||
Loading…
Reference in a new issue