Compare commits

..

No commits in common. "master" and "v0.3.1" have entirely different histories.

13 changed files with 512 additions and 1096 deletions

2
.gitignore vendored
View file

@ -9,5 +9,3 @@ pom.xml.asc
/.nrepl-port /.nrepl-port
.hgignore .hgignore
.hg/ .hg/
.lsp/
.clj-kondo/

View file

@ -1,44 +1,11 @@
# Change Log # Change Log
All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/).
## Unreleased ## [0.3.1]
## 0.8.0 - 2024-02-07
### Added
- Support for passing `:realise-fn` to avoid lazy response (thanks @jimpil)
### Changed
- Remove reflective calls
## 0.7.0 - 2022-10-04
### Added
- Support reading dates as instanceres (thanks @henryw374)
- Support data literal for mongo id (thanks @henryw374)
- Support for implicit transactions (thanks @AdamClements)
- Support for aggregation pipeline in find-one-and-update (requires 4.2+, thanks @jacobemcken)
## 0.6.0 - 2020-01-10
### Added
- Support for bulk-write
### Changed
- Moved option creators and document conversion to the `mongo-driver-3.model` namespace (breaking change)
## 0.5.0 - 2019-11-22
### Added
- Support for transactions
## 0.4.0 - 2019-11-19
### Added
- list collections
- start session
- remove reflection warnings
## 0.3.1 - 2019-11-17
### Added ### Added
- More documentation - More documentation
## 0.3.0 - 2019-11-15 ## [0.3.0] - 2019-11-15
### Added ### Added
- Added aggregate function - Added aggregate function
- `skip` option to `find` - `skip` option to `find`
@ -49,10 +16,15 @@ All notable changes to this project will be documented in this file. This change
- Added ? suffix to boolean params - Added ? suffix to boolean params
- Renamed `find-one-as-map` to `find-one` - Renamed `find-one-as-map` to `find-one`
## 0.2.0 - 2019-11-14 ## [0.2.0] - 2019-11-14
### Added ### Added
- expose operators - expose operators
## 0.1.0 - 2019-11-14 ## 0.1.0 - 2019-11-14
### Added ### Added
- Initial release - Initial release
[Unreleased]: https://github.com/gnarroway/mongo-driver-3/compare/v0.3.1...HEAD
[0.3.1]: https://github.com/gnarroway/mongo-driver-3/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/gnarroway/mongo-driver-3/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/gnarroway/hato/compare/v0.1.0...v0.2.0

107
README.md
View file

@ -2,10 +2,10 @@
[![Clojars Project](https://img.shields.io/clojars/v/mongo-driver-3.svg)](https://clojars.org/mongo-driver-3) [![Clojars Project](https://img.shields.io/clojars/v/mongo-driver-3.svg)](https://clojars.org/mongo-driver-3)
[![cljdoc badge](https://cljdoc.org/badge/mongo-driver-3/mongo-driver-3)](https://cljdoc.org/d/mongo-driver-3/mongo-driver-3/CURRENT) [![cljdoc badge](https://cljdoc.org/badge/mongo-driver-3/mongo-driver-3)](https://cljdoc.org/d/manifold/manifold/CURRENT)
A Mongo client for clojure, lightly wrapping 3.11/4.0+ versions of the [MongoDB Java Driver](https://mongodb.github.io/mongo-java-driver/) 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). 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 Like our HTTP/2 client [hato](https://github.com/gnarroway/hato), the API is designed to be idiomatic and to make common
@ -23,7 +23,7 @@ It was developed with the following goals:
## Status ## Status
mongo-driver-3 is used in production, and the existing public API will be maintained. mongo-driver-3 is under active development and the API may change.
Please try it out and raise any issues you may find. Please try it out and raise any issues you may find.
## Usage ## Usage
@ -32,23 +32,21 @@ For Leinengen, add this to your project.clj:
```clojure ```clojure
;; The underlying driver -- any newer version can also be used ;; The underlying driver -- any newer version can also be used
[org.mongodb/mongodb-driver-sync "4.11.1"] [org.mongodb/mongodb-driver-sync "3.11.2"]
;; This wrapper library ;; This wrapper library
[mongo-driver-3 "0.8.0"] [mongo-driver-3 "0.3.1"]
``` ```
## Getting started ## Getting started
```clojure
(ns my.app
(:require [mongo-driver-3.client :as mcl]))
```
We usually start by creating a client and connecting to a database with a connection string. We usually start by creating a client and connecting to a database with a connection string.
`connect-to-db` is a convenience function that allows you to do this directly. `connect-to-db` is a convenience function that allows you to do this directly.
```clojure ```clojure
(ns my.app
(:require [mongo-driver-3.client :as mcl]))
(mcl/connect-to-db "mongodb://localhost:27017/my-db") (mcl/connect-to-db "mongodb://localhost:27017/my-db")
; => ; =>
; { ; {
@ -57,19 +55,19 @@ We usually start by creating a client and connecting to a database with a connec
; } ; }
``` ```
You can also create a client and get a DB separately: You can also create a client and get a DB manually:
```clojure ```clojure
;; Calling create without an arg will try and connect to the default host/port. ;; Calling create without an arg will try and connect to the default host/port.
(def client (mcl/create "mongodb://localhost:27017")) (def client (mcl/create "mongodb://localhost:27017"))
;; Create a db that you can pass around. ;; Create a db that you can pass around.
(def db (mcl/get-db client "my-db")) (def db (mcl/get-db client "my-db:))
``` ```
### Collection functions ### Collection functions
All the collection functions closely mirror the naming in the corresponding java driver All the collection functions closely mirror the name of the corresponding java driver
[module](https://mongodb.github.io/mongo-java-driver/3.11/javadoc/com/mongodb/client/MongoCollection.html). [module](https://mongodb.github.io/mongo-java-driver/3.11/javadoc/com/mongodb/client/MongoCollection.html).
They always take a db as the first argument, collection name as the second, They always take a db as the first argument, collection name as the second,
@ -94,25 +92,20 @@ As an example:
; => 1 ; => 1
;; Find the documents, returning a seq ;; Find the documents, returning a seq
(mc/find db "test" {} {:limit 1 :projection {:_id 0}}) (mc/find db "test' {} {:limit 1 :projection {:_id 0}})
; => ({:v "hello"}) ; => ({:v "hello"})
;; Find the documents, returning the raw FindIterable response ;; Find the documents, returning the raw FindIterable response
(mc/find db "test" {} {:raw? true}) (mc/find db "test' {} {:raw? true})
; => a MongoIterable ; => a MongoIterable
;; Find a single document or return nil ;; Find a single document or return nil
(mc/find-one db "test" {:v "world"} {:keywordize? false}) (mc/find-one db "test' {:v "world"} {:keywordize? false})
; => {"v" "world"} ; => {"v" "world"}
;; Avoid laziness in queries
(mc/find db "test" {} {:realise-fn (partial into [])}
; => [...]
``` ```
While most options are supported directly, sometimes you may need to some extra control. While most options are supported directly, sometimes you may need to configure an operation directly.
In such cases, you can pass in a configured java options object. Any other In such cases, you can pass in the java options object.
options will be applied on top of this object.
```clojure ```clojure
;; These are equivalent ;; These are equivalent
@ -124,74 +117,6 @@ options will be applied on top of this object.
Again, read the [docs](https://cljdoc.org/d/mongo-driver-3/mongo-driver-3/CURRENT/api/mongo-driver-3.collection) Again, read the [docs](https://cljdoc.org/d/mongo-driver-3/mongo-driver-3/CURRENT/api/mongo-driver-3.collection)
for full API documentation. for full API documentation.
### Using operators
Many mongo queries take operators like `$eq` and `$gt`. These are exposed in the `mongo-driver-3.operator` namespace.
```clojure
(ns my.app
(:require [mongo-driver-3.collection :as mc]
[mongo-driver-3.operator :refer [$gt]))
(mc/find db "test" {:a {$gt 3}})
;; This is equivalent to, but with less chance of error than:
(mc/find db "test" {:a {"$gt" 3}})
```
### Bulk operations
The bulk API is similar to the [mongo shell](https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/),
except each operation is defined as a 2-tuple rather than a map.
```clojure
;; Execute a mix of operations in one go
(bulk-write [[:insert-one {:document {:a 1}}]
[:delete-one {:filter {:a 1}}]
[:delete-many {:filter {:a 1}}]
[:update-one {:filter {:a 1} :update {:$set {:a 2}}}]
[:update-many {:filter {:a 1} :update {:$set {:a 2}}}]
[:replace-one {:filter {:a 1} :replacement {:a 2}}]])
; => a BulkWriteResult
;; Each operation can take the same options as their respective functions
(bulk-write [[:update-one {:filter {:a 1} :update {:$set {:a 2}} :upsert? true}]
[:update-many {:filter {:a 1} :update {:$set {:a 2}} :upsert? true}]
[:replace-one {:filter {:a 1} :replacement {:a 2} :upsert? true}]])
```
### Using transactions
You can create a session to perform multi-document transactions, where all operations either
succeed or none are persisted.
It is important to
use `with-open` so the session is closed after both successful and failed transactions.
```clojure
;; Inserts 2 documents into a collection
(with-open [s (mg/start-session client)]
(mg/with-transaction s
(fn []
(mc/insert-one my-db "coll" {:name "hello"} {:session s})
(mc/insert-one my-db "coll" {:name "world"} {:session s}))))
;; There is also a helper method to make this easier,
;; where it is not necessary to manually open or pass a session:
(mg/with-implicit-transaction
{:client client}
(fn []
(mc/insert-one my-db "coll" {:name "hello"})
(mc/insert-one my-db "coll" {:name "world"})))
```
## Development
1. Run mongo (e.g. via docker):
- `docker run -it --rm -p 27017:27017 mongo`
2. Run tests
- `lein test`
## License ## License
Released under the MIT License: http://www.opensource.org/licenses/mit-license.php Released under the MIT License: http://www.opensource.org/licenses/mit-license.php

View file

@ -1,5 +1,5 @@
(defproject mongo-driver-3 "0.9.0-SNAPSHOT" (defproject mongo-driver-3 "0.3.1"
:description "A Clojure wrapper for the Java MongoDB driver 3.11/4.0+." :description "A Clojure wrapper for the Java MongoDB driver 3.11+."
:url "https://github.com/gnarroway/mongo-driver-3" :url "https://github.com/gnarroway/mongo-driver-3"
:license {:name "The MIT License" :license {:name "The MIT License"
:url "http://opensource.org/licenses/mit-license.php" :url "http://opensource.org/licenses/mit-license.php"
@ -10,5 +10,5 @@
:sign-releases false}]] :sign-releases false}]]
:plugins [[lein-cljfmt "0.6.4"]] :plugins [[lein-cljfmt "0.6.4"]]
:profiles {:dev {:dependencies [[org.clojure/clojure "1.11.1"] :profiles {:dev {:dependencies [[org.clojure/clojure "1.10.1"]
[org.mongodb/mongodb-driver-sync "4.11.1"]]}}) [org.mongodb/mongodb-driver-sync "3.11.0"]]}})

View file

@ -1 +0,0 @@
{mongo/id mongo-driver-3.data-literals/mongo-id}

View file

@ -1,12 +1,7 @@
(ns mongo-driver-3.client (ns mongo-driver-3.client
(:refer-clojure :exclude [find]) (:refer-clojure :exclude [find])
(:require [mongo-driver-3.model :as m] (:import (com.mongodb.client MongoClients MongoClient)
[mongo-driver-3.iterable :as iterable]) (com.mongodb ConnectionString)))
(:import (com.mongodb.client MongoClients MongoClient ClientSession MongoDatabase TransactionBody)
(com.mongodb ConnectionString ClientSessionOptions TransactionOptions)
(java.util.concurrent TimeUnit)))
(set! *warn-on-reflection* true)
;;; Core ;;; Core
@ -16,15 +11,16 @@
`connection-string` is a mongo connection string, e.g. mongodb://localhost:27107 `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." If a connecting string is not passed in, it will connect to the default localhost instance."
(^MongoClient [] (MongoClients/create)) ([] (MongoClients/create))
(^MongoClient [^String connection-string] (MongoClients/create connection-string))) ([^String connection-string]
(MongoClients/create connection-string)))
(defn get-db (defn get-db
"Gets a database by name "Gets a database by name
`client` is a MongoClient, e.g. resulting from calling `connect` `client` is a MongoClient, e.g. resulting from calling `connect`
`name` is the name of the database to get." `name` is the name of the database to get."
^MongoDatabase [^MongoClient client ^String name] [^MongoClient client ^String name]
(.getDatabase client name)) (.getDatabase client name))
(defn close (defn close
@ -32,154 +28,6 @@
[^MongoClient client] [^MongoClient client]
(.close client)) (.close client))
(defn list-collections
"Lists collections in a database, returning as a seq of maps unless otherwise configured.
Arguments:
- `db` a MongoDatabase
- `opts` (optional), a map of:
- `:name-only?` returns just the string names
- `:keywordize?` keywordize the keys of return results, default: true. Only applicable if `:name-only?` is false.
- `:raw?` return the mongo iterable directly instead of processing into a clj data-structure, default: false
- `:realise-fn` how to realise the MongoIterable, default: `clojure.core/sequence` (i.e. lazily)
- `:session` a ClientSession"
([^MongoDatabase db] (list-collections db {}))
([^MongoDatabase db {:keys [raw? keywordize? ^ClientSession session realise-fn]
:or {keywordize? true
realise-fn sequence}}]
(let [it (if session
(.listCollections db session)
(.listCollections db))]
(if raw?
it
(realise-fn ;; accomodate users who don't want to use lazy-seqs
(iterable/documents it keywordize?))))))
(defn list-collection-names
"Lists collection names in a database, returning as a seq of strings unless otherwise configured.
Arguments:
- `db` a MongoDatabase
- `opts` (optional), a map of:
- `:raw?` return the mongo MongoIterable directly instead of processing into a seq, default: false
- `:session` a ClientSession"
([^MongoDatabase db] (list-collection-names db {}))
([^MongoDatabase db opts]
(let [it (if-let [^ClientSession session (:session opts)]
(.listCollectionNames db session)
(.listCollectionNames db))]
(if (:raw? opts)
it
(seq it)))))
(defn ->TransactionOptions
"Coerces options map into a TransactionOptions. See `start-session` for usage."
[{:keys [max-commit-time-ms] :as opts}]
(let [rp (m/->ReadPreference opts)
rc (m/->ReadConcern opts)
wc (m/->WriteConcern opts)]
(cond-> (TransactionOptions/builder)
max-commit-time-ms (.maxCommitTime max-commit-time-ms (TimeUnit/MILLISECONDS))
rp (.readPreference rp)
rc (.readConcern rc)
wc (.writeConcern wc)
true (.build))))
(defn ->ClientSessionOptions
"Coerces an options map into a ClientSessionOptions See `start-session` for usage.
See `start-session` for usage"
[{:keys [client-session-options causally-consistent?] :as opts}]
(let [trans-opts (->TransactionOptions opts)]
(cond-> (if client-session-options (ClientSessionOptions/builder client-session-options) (ClientSessionOptions/builder))
trans-opts (.defaultTransactionOptions trans-opts)
(some? causally-consistent?) (.causallyConsistent causally-consistent?)
true (.build))))
(defn start-session
"Creates a client session.
Arguments:
- `client` a MongoClient
- `opts` (optional), a map of:
- `:max-commit-time-ms` Max execution time for commitTransaction operation, in milliseconds
- `:causally-consistent?` whether operations using session should be causally consistent with each other
- `:read-preference` Accepts a ReadPreference or a kw corresponding to one:
[:primary, :primaryPreferred, :secondary, :secondaryPreferred, :nearest]
Invalid values will throw an exception.
- `:read-concern` Accepts a ReadConcern or kw corresponding to one:
[:available, :default, :linearizable, :local, :majority, :snapshot]
Invalid values will throw an exception.
- `: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.
- `:client-session-options` a ClientSessionOptions, for configuring directly. If specified, any
other [preceding] query options will be applied to it."
([^MongoClient client] (start-session client {}))
([^MongoClient client opts]
(.startSession client (->ClientSessionOptions opts))))
(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.
```clojure
(with-open [s (start-session client)]
(with-transaction s
(fn []
(insert-one my-db \"coll\" {:name \"hello\"} {:session s})
(insert-one my-db \"coll\" {:name \"world\"} {:session s}))))
```"
([^ClientSession session body] (with-transaction session body {}))
([^ClientSession session body opts]
(.withTransaction session
(reify TransactionBody
(execute [_] (body)))
(->TransactionOptions opts))))
(def ^:dynamic *session* nil)
(defn with-implicit-transaction
"Automatically sets the session / transaction for all mongo operations
within the scope, using a dynamic binding.
The first argument is an options map with keys:
- `:client` a MongoClient (mandatory)
- `:transaction-opts` (see `->TransactionOptions` for keys)
- `:session-opts` (see `start-session` for details)
The second argument `body` is a fn with one or more mongo operations in it.
e.g.
```
(mg/with-implicit-transaction
{:client client}
(fn []
(mc/insert-one my-db \"coll\" {:name \"hello\"})
(mc/insert-one my-db \"coll\" {:name \"world\"})))
```"
[{:keys [^MongoClient client transaction-opts session-opts] :or {transaction-opts {} session-opts {}}} body]
(with-open [^ClientSession session (start-session client session-opts)]
(binding [*session* session]
(with-transaction
*session*
body
transaction-opts))))
;;; Utility ;;; Utility
(defn connect-to-db (defn connect-to-db

View file

@ -1,19 +1,122 @@
(ns mongo-driver-3.collection (ns mongo-driver-3.collection
(:refer-clojure :exclude [find empty? drop]) (:refer-clojure :exclude [find empty? drop])
(:require [mongo-driver-3.model :refer :all] (:import (clojure.lang Ratio Keyword Named IPersistentMap)
[mongo-driver-3.client :refer [*session*]] (com.mongodb ReadConcern ReadPreference WriteConcern MongoNamespace)
[mongo-driver-3.iterable :as iterable]) (com.mongodb.client MongoDatabase MongoCollection TransactionBody)
(:import (com.mongodb MongoNamespace) (com.mongodb.client.model InsertOneOptions InsertManyOptions DeleteOptions FindOneAndUpdateOptions ReturnDocument FindOneAndReplaceOptions CountOptions CreateCollectionOptions RenameCollectionOptions IndexOptions IndexModel UpdateOptions ReplaceOptions)
(com.mongodb.client MongoDatabase MongoCollection ClientSession) (java.util List Collection)
(com.mongodb.client.model IndexModel) (java.util.concurrent TimeUnit)
(java.util List) (org.bson Document)
(org.bson Document))) (org.bson.types Decimal128)))
;;; Conversions
(defprotocol ConvertToDocument
(^Document document [input] "Convert from clojure to 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 Mongo 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))))
(set! *warn-on-reflection* true)
;;; Collection ;;; Collection
(defn ^MongoCollection 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. See `collection` for usage."
[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. See `collection` for usage."
[rp]
(when rp
(if (instance? ReadPreference rp)
rp
(ReadPreference/valueOf (name rp)))))
(defn ->WriteConcern
"Coerces write-concern related options to a WriteConcern. See `collection` for usage."
[{: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 w (.withW % w) %))
(#(if w-timeout-ms (.withWTimeout % w-timeout-ms (TimeUnit/MILLISECONDS)) %))
(#(if (some? journal?) (.withJournal % journal?) %))))))
(defn collection
"Coerces `coll` to a MongoCollection with some options. "Coerces `coll` to a MongoCollection with some options.
Arguments: Arguments:
@ -38,14 +141,12 @@
([^MongoDatabase db coll] ([^MongoDatabase db coll]
(collection db coll {})) (collection db coll {}))
([^MongoDatabase db coll opts] ([^MongoDatabase db coll opts]
(let [^MongoCollection coll' (if (instance? MongoCollection coll) coll (.getCollection db coll)) (let [coll' (if (instance? MongoCollection coll) coll (.getCollection db coll))
rp (->ReadPreference opts) {:keys [read-concern read-preference]} opts]
rc (->ReadConcern opts) (-> coll'
wc (->WriteConcern opts)] (#(if-let [rp (->ReadPreference read-preference)] (.withReadPreference % rp) %))
(cond-> ^MongoCollection coll' (#(if-let [rc (->ReadConcern read-concern)] (.withReadConcern % rc) %))
rp (.withReadPreference rp) (#(if-let [wc (->WriteConcern opts)] (.withWriteConcern % wc) %))))))
rc (.withReadConcern rc)
wc (.withWriteConcern wc)))))
;;; CRUD functions ;;; CRUD functions
@ -63,55 +164,33 @@
- `:batch-size` Documents to return per batch, e.g. 1 - `:batch-size` Documents to return per batch, e.g. 1
- `:bypass-document-validation?` Boolean - `:bypass-document-validation?` Boolean
- `:keywordize?` keywordize the keys of return results, default: true - `:keywordize?` keywordize the keys of return results, default: true
- `:realise-fn` how to realise the MongoIterable, default: `clojure.core/sequence` (i.e. lazily)
- `:raw?` return the mongo AggregateIterable directly instead of processing into a seq, default: false - `:raw?` return the mongo AggregateIterable directly instead of processing into a seq, default: false
- `:session` a ClientSession" - `:session` a ClientSession"
([^MongoDatabase db coll pipeline] ([^MongoDatabase db coll pipeline]
(aggregate db coll pipeline {})) (aggregate db coll pipeline {}))
([^MongoDatabase db coll pipeline opts] ([^MongoDatabase db coll pipeline opts]
(let [{:keys [^ClientSession session allow-disk-use? ^Integer batch-size bypass-document-validation? keywordize? raw? realise-fn] (let [{:keys [session allow-disk-use? batch-size bypass-document-validation? keywordize? raw?] :or {keywordize? true raw? false}} opts
:or {keywordize? true it (-> (if session
realise-fn sequence}} opts (.aggregate (collection db coll opts) session (document pipeline))
^ClientSession session (or session *session*) (.aggregate (collection db coll opts) (document pipeline)))
it (cond-> (if session (#(if (some? allow-disk-use?) (.allowDiskUse % allow-disk-use?) %))
(.aggregate (collection db coll opts) session ^List (map document pipeline)) (#(if batch-size (.batchSize % batch-size) %))
(.aggregate (collection db coll opts) ^List (map document pipeline))) (#(if (some? bypass-document-validation?) (.bypassDocumentValidation % bypass-document-validation?) %)))]
(some? allow-disk-use?) (.allowDiskUse allow-disk-use?)
(some? bypass-document-validation?) (.bypassDocumentValidation bypass-document-validation?)
batch-size (.batchSize batch-size))]
(if raw? (if-not raw?
it (map (fn [x] (from-document x keywordize?)) (seq it))
(realise-fn ;; accomodate users who don't want to use lazy-seqs it))))
(iterable/documents it keywordize?))))))
(defn bulk-write (defn ->CountOptions
"Executes a mix of inserts, updates, replaces, and deletes. "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))
- `db` is a MongoDatabase opts))
- `coll` is a collection name
- `operations` a list of 2-tuples in the form `[op config]`,
- `op` is one of :insert-one :update-one :update-many :delete-one :delete-many :replace-one
- `config` the configuration map for the operation
- `insert` takes `:document`
- `update` takes `:filter`, `:update`, and any options in the corresponding update function
- `delete` takes `:filter`, and any options in the corresponding delete function
- `replace` takes `:filter`, `:replacement`, and any options in the corresponding replace function
- `opts` (optional), a map of:
- `:bypass-document-validation?` Boolean
- `:ordered?` Boolean whether serve should insert documents in order provided (default true)
- `:bulk-write-options` A BulkWriteOptions for configuring directly. If specified,
any other [preceding] query options will be applied to it.
- `:session` A ClientSession
Additionally takes options specified in `collection`"
([^MongoDatabase db coll operations]
(bulk-write db coll operations {}))
([^MongoDatabase db coll operations opts]
(let [opts' (->BulkWriteOptions opts)]
(if-let [session (or (:session opts) *session*)]
(.bulkWrite (collection db coll opts') ^ClientSession session ^List (map write-model operations))
(.bulkWrite (collection db coll opts') (map write-model operations))))))
(defn count-documents (defn count-documents
"Count documents in a collection, optionally matching a filter query `q`. "Count documents in a collection, optionally matching a filter query `q`.
@ -137,10 +216,16 @@
(count-documents db coll q {})) (count-documents db coll q {}))
([^MongoDatabase db coll q opts] ([^MongoDatabase db coll q opts]
(let [opts' (->CountOptions opts)] (let [opts' (->CountOptions opts)]
(if-let [session (or (:session opts) *session*)] (if-let [session (:session opts)]
(.countDocuments (collection db coll opts) session (document q) opts') (.countDocuments (collection db coll opts) session (document q) opts')
(.countDocuments (collection db coll opts) (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 (defn delete-one
"Deletes a single document from a collection and returns a DeleteResult. "Deletes a single document from a collection and returns a DeleteResult.
@ -157,7 +242,7 @@
([^MongoDatabase db coll q] ([^MongoDatabase db coll q]
(delete-one db coll q {})) (delete-one db coll q {}))
([^MongoDatabase db coll q opts] ([^MongoDatabase db coll q opts]
(if-let [session (or (:session opts) *session*)] (if-let [session (:session opts)]
(.deleteOne (collection db coll opts) session (document q) (->DeleteOptions opts)) (.deleteOne (collection db coll opts) session (document q) (->DeleteOptions opts))
(.deleteOne (collection db coll opts) (document q) (->DeleteOptions opts))))) (.deleteOne (collection db coll opts) (document q) (->DeleteOptions opts)))))
@ -177,7 +262,7 @@
([^MongoDatabase db coll q] ([^MongoDatabase db coll q]
(delete-many db coll q {})) (delete-many db coll q {}))
([^MongoDatabase db coll q opts] ([^MongoDatabase db coll q opts]
(if-let [session (or (:session opts) *session*)] (if-let [session (:session opts)]
(.deleteMany (collection db coll opts) session (document q) (->DeleteOptions opts)) (.deleteMany (collection db coll opts) session (document q) (->DeleteOptions opts))
(.deleteMany (collection db coll opts) (document q) (->DeleteOptions opts))))) (.deleteMany (collection db coll opts) (document q) (->DeleteOptions opts)))))
@ -195,30 +280,25 @@
- `:sort` document representing sort order, e.g. {:timestamp -1} - `:sort` document representing sort order, e.g. {:timestamp -1}
- `:projection` document representing fields to return, e.g. {:_id 0} - `:projection` document representing fields to return, e.g. {:_id 0}
- `:keywordize?` keywordize the keys of return results, default: true - `:keywordize?` keywordize the keys of return results, default: true
- `:realise-fn` how to realise the MongoIterable, default: `clojure.core/sequence` (i.e. lazily)
- `:raw?` return the mongo FindIterable directly instead of processing into a seq, default: false - `:raw?` return the mongo FindIterable directly instead of processing into a seq, default: false
- `:session` a ClientSession - `:session` a ClientSession
Additionally takes options specified in `collection`." Additionally takes options specified in `collection`."
([^MongoDatabase db coll q] ([^MongoDatabase db coll q]
(find db coll q {})) (find db coll q {}))
([^MongoDatabase db coll q {:keys [limit skip sort projection ^ClientSession session keywordize? raw? realise-fn] ([^MongoDatabase db coll q opts]
:or {keywordize? true (let [{:keys [limit skip sort projection session keywordize? raw?] :or {keywordize? true raw? false}} opts]
realise-fn sequence} (let [it (-> (if session
:as opts}] (.find (collection db coll opts) session (document q))
(let [^ClientSession session (or session *session*) (.find (collection db coll opts) (document q)))
it (cond-> (if session (#(if limit (.limit % limit) %))
(.find (collection db coll opts) session (document q)) (#(if skip (.skip % skip) %))
(.find (collection db coll opts) (document q))) (#(if sort (.sort % (document sort)) %))
limit (.limit limit) (#(if projection (.projection % (document projection)) %)))]
skip (.skip skip)
sort (.sort (document sort))
projection (.projection (document projection)))]
(if raw? (if-not raw?
it (map (fn [x] (from-document x keywordize?)) (seq it))
(realise-fn ;; accomodate users who don't want to use lazy-seqs it)))))
(iterable/documents it keywordize?))))))
(defn find-one (defn find-one
"Finds a single document and returns it as a clojure map, or nil if not found. "Finds a single document and returns it as a clojure map, or nil if not found.
@ -229,6 +309,17 @@
([^MongoDatabase db coll q opts] ([^MongoDatabase db coll q opts]
(first (find db coll q (assoc opts :limit 1 :raw? false))))) (first (find db coll q (assoc opts :limit 1 :raw? false)))))
(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 (defn find-one-and-update
"Atomically find a document (at most one) and modify it. "Atomically find a document (at most one) and modify it.
@ -237,10 +328,7 @@
- `db` is a MongoDatabase - `db` is a MongoDatabase
- `coll` is a collection name - `coll` is a collection name
- `q` is a map representing a query. - `q` is a map representing a query.
- `update` is either a map representing a document update or a vector - `update` is a map representing an update. The update to apply must include only update operators.
representing an 'aggregation pipeline'. A document update must include only
update operators, while an 'aggregation pipeline' can contain multiple
stages of `$set`, `$unset` and `$replaceWith`.
- `opts` (optional), a map of: - `opts` (optional), a map of:
- `:upsert?` whether to insert a new document if nothing is found, default: false - `: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 - `:return-new?` whether to return the document after update (insead of its state before the update), default: false
@ -255,19 +343,24 @@
([^MongoDatabase db coll q update] ([^MongoDatabase db coll q update]
(find-one-and-update db coll q update {})) (find-one-and-update db coll q update {}))
([^MongoDatabase db coll q update opts] ([^MongoDatabase db coll q update opts]
(let [{:keys [keywordize? ^ClientSession session] :or {keywordize? true}} opts (let [{:keys [keywordize? session] :or {keywordize? true}} opts
^ClientSession session (or session *session*) opts' (->FindOneAndUpdateOptions opts)]
opts' (->FindOneAndUpdateOptions opts)] (-> (if session
(-> (if (instance? List update) (.findOneAndUpdate (collection db coll opts) session (document q) (document update) opts')
(let [pipeline ^List (map document update)] (.findOneAndUpdate (collection db coll opts) (document q) (document update) opts'))
(if session
(.findOneAndUpdate (collection db coll opts) session (document q) pipeline opts')
(.findOneAndUpdate (collection db coll opts) (document q) pipeline 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?))))) (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 (defn find-one-and-replace
"Atomically find a document (at most one) and replace it. "Atomically find a document (at most one) and replace it.
@ -292,13 +385,20 @@
(find-one-and-replace db coll q doc {})) (find-one-and-replace db coll q doc {}))
([^MongoDatabase db coll q doc opts] ([^MongoDatabase db coll q doc opts]
(let [{:keys [keywordize? session] :or {keywordize? true}} opts (let [{:keys [keywordize? session] :or {keywordize? true}} opts
session (or session *session*) opts' (->FindOneAndReplaceOptions opts)]
opts' (->FindOneAndReplaceOptions opts)]
(-> (if session (-> (if session
(.findOneAndReplace (collection db coll opts) session (document q) (document doc) opts') (.findOneAndReplace (collection db coll opts) session (document q) (document doc) opts')
(.findOneAndReplace (collection db coll opts) (document q) (document doc) opts')) (.findOneAndReplace (collection db coll opts) (document q) (document doc) opts'))
(from-document keywordize?))))) (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 (defn insert-one
"Inserts a single document into a collection, and returns nil. "Inserts a single document into a collection, and returns nil.
If the document does not have an _id field, it will be auto-generated by the underlying driver. If the document does not have an _id field, it will be auto-generated by the underlying driver.
@ -319,10 +419,19 @@
(insert-one db coll doc {})) (insert-one db coll doc {}))
([^MongoDatabase db coll doc opts] ([^MongoDatabase db coll doc opts]
(let [opts' (->InsertOneOptions opts)] (let [opts' (->InsertOneOptions opts)]
(if-let [session (or (:session opts) *session*)] (if-let [session (:session opts)]
(.insertOne (collection db coll opts) session (document doc) opts') (.insertOne (collection db coll opts) session (document doc) opts')
(.insertOne (collection db coll opts) (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 (defn insert-many
"Inserts multiple documents into a collection. "Inserts multiple documents into a collection.
If a document does not have an _id field, it will be auto-generated by the underlying driver. If a document does not have an _id field, it will be auto-generated by the underlying driver.
@ -344,9 +453,18 @@
(insert-many db coll docs {})) (insert-many db coll docs {}))
([^MongoDatabase db coll docs opts] ([^MongoDatabase db coll docs opts]
(let [opts' (->InsertManyOptions opts)] (let [opts' (->InsertManyOptions opts)]
(if-let [^ClientSession session (or (:session opts) *session*)] (if-let [session (:session opts)]
(.insertMany (collection db coll opts) session ^List (map document docs) opts') (.insertMany (collection db coll opts) session (map document docs) opts')
(.insertMany (collection db coll opts) ^List (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 (defn replace-one
"Replace a single document in a collection and returns an UpdateResult. "Replace a single document in a collection and returns an UpdateResult.
@ -368,10 +486,19 @@
([^MongoDatabase db coll q doc] ([^MongoDatabase db coll q doc]
(find-one-and-replace db coll q doc {})) (find-one-and-replace db coll q doc {}))
([^MongoDatabase db coll q doc opts] ([^MongoDatabase db coll q doc opts]
(if-let [^ClientSession session (or (:session opts) *session*)] (if-let [session (:session opts)]
(.replaceOne (collection db coll opts) session (document q) (document doc) (->ReplaceOptions opts)) (.replaceOne (collection db coll opts) session (document q) (document doc) (->ReplaceOptions opts))
(.replaceOne (collection db coll opts) (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 `update-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 (defn update-one
"Updates a single document in a collection and returns an UpdateResult. "Updates a single document in a collection and returns an UpdateResult.
@ -392,7 +519,7 @@
([^MongoDatabase db coll q update] ([^MongoDatabase db coll q update]
(update-one db coll q update {})) (update-one db coll q update {}))
([^MongoDatabase db coll q update opts] ([^MongoDatabase db coll q update opts]
(if-let [^ClientSession session (or (:session opts) *session*)] (if-let [session (:session opts)]
(.updateOne (collection db coll opts) session (document q) (document update) (->UpdateOptions opts)) (.updateOne (collection db coll opts) session (document q) (document update) (->UpdateOptions opts))
(.updateOne (collection db coll opts) (document q) (document update) (->UpdateOptions opts))))) (.updateOne (collection db coll opts) (document q) (document update) (->UpdateOptions opts)))))
@ -414,14 +541,24 @@
Additionally takes options specified in `collection`" Additionally takes options specified in `collection`"
([^MongoDatabase db coll q update] ([^MongoDatabase db coll q update]
(update-many db coll q update {})) (update-many db coll q {}))
([^MongoDatabase db coll q update opts] ([^MongoDatabase db coll q update opts]
(if-let [^ClientSession session (or (:session opts) *session*)] (if-let [session (:session opts)]
(.updateMany (collection db coll opts) session (document q) (document update) (->UpdateOptions opts)) (.updateMany (collection db coll opts) session (document q) (document update) (->UpdateOptions opts))
(.updateMany (collection db coll opts) (document q) (document update) (->UpdateOptions opts))))) (.updateMany (collection db coll opts) (document q) (document update) (->UpdateOptions opts)))))
;;; Admin functions ;;; 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 (defn create
"Creates a collection "Creates a collection
@ -435,12 +572,20 @@
- `:max-size-bytes` max collection size in bytes 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, - `:create-collection-options` A CreateCollectionOptions for configuring directly. If specified,
any other [preceding] query options will be applied to it" any other [preceding] query options will be applied to it"
([^MongoDatabase db ^String coll] ([^MongoDatabase db coll]
(create db coll {})) (create db coll {}))
([^MongoDatabase db ^String coll opts] ([^MongoDatabase db coll opts]
(let [opts' (->CreateCollectionOptions opts)] (let [opts' (->CreateCollectionOptions opts)]
(.createCollection db coll opts')))) (.createCollection db coll opts'))))
(defn ->RenameCollectionOptions
"Coerce options map into RenameCollectionOptions. See `rename` usage."
[{:keys [rename-collection-options drop-target?]}]
(let [opts (or rename-collection-options (RenameCollectionOptions.))]
(when (some? drop-target?) (.dropTarget opts drop-target?))
opts))
(defn rename (defn rename
"Renames `coll` to `new-coll` in the same DB. "Renames `coll` to `new-coll` in the same DB.
@ -467,6 +612,18 @@
[^MongoDatabase db coll] [^MongoDatabase db coll]
(.drop (collection 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 (defn create-index
"Creates an index "Creates an index
@ -502,7 +659,7 @@
(create-indexes db coll indexes {})) (create-indexes db coll indexes {}))
([^MongoDatabase db coll indexes opts] ([^MongoDatabase db coll indexes opts]
(->> indexes (->> indexes
(mapv (fn [x] (IndexModel. (document (:keys x)) (->IndexOptions x)))) (map (fn [x] (IndexModel. (document (:keys x)) (->IndexOptions x))))
(.createIndexes (collection db coll opts))))) (.createIndexes (collection db coll opts)))))
(defn list-indexes (defn list-indexes
@ -510,7 +667,23 @@
([^MongoDatabase db coll] ([^MongoDatabase db coll]
(list-indexes db coll {})) (list-indexes db coll {}))
([^MongoDatabase db coll opts] ([^MongoDatabase db coll opts]
(let [it (.listIndexes (collection db coll opts)) (->> (.listIndexes (collection db coll opts))
realise-fn (:realise-fn opts sequence)] (map #(from-document % true)))))
(realise-fn
(iterable/documents it true))))) ;;; Utility functions
(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))))

View file

@ -1,36 +0,0 @@
(ns mongo-driver-3.data-literals
(:import (org.bson.types ObjectId)
(java.io Writer)
(java.util Date)
(java.nio ByteBuffer)))
(defmethod print-method ObjectId [^ObjectId c ^Writer w] (.write w (str "#mongo/id " \" (.toHexString c) \")))
(defmethod print-dup ObjectId [^ObjectId c ^Writer w] (.write w (str "#mongo/id " \" (.toHexString c) \")))
(defprotocol AsObjectId
(oid-from [this]))
(extend-protocol AsObjectId
(Class/forName "[B")
(oid-from [this] (ObjectId. ^bytes this))
nil
(oid-from [_] (ObjectId.))
String
(oid-from [this] (ObjectId. this))
Date
(oid-from [this] (ObjectId. this))
ByteBuffer
(oid-from [this] (ObjectId. this))
)
(defn mongo-id ;; https://mongodb.github.io/mongo-java-driver/4.8/apidocs/bson/org/bson/types/ObjectId.html
(^ObjectId [] (ObjectId.))
(^ObjectId [o] (oid-from o))
(^ObjectId [o1 o2]
(if (and (int? o1)
(int? o2))
(ObjectId. (int o1) (int o2))
(ObjectId. ^Date o1 (int o2)))))

View file

@ -1,8 +0,0 @@
(ns mongo-driver-3.iterable
(:require [mongo-driver-3.model :as m]))
(defn documents
"Given a MongoIterable <it>, returns an eduction which will
eventually yield all the documents (per `m/from-document`)."
[it keywordize?]
(eduction (map #(m/from-document % keywordize?)) it))

View file

@ -1,251 +0,0 @@
(ns mongo-driver-3.model
(:import (com.mongodb.client.model CountOptions DeleteOptions ReturnDocument FindOneAndUpdateOptions InsertOneOptions ReplaceOptions UpdateOptions CreateCollectionOptions RenameCollectionOptions InsertManyOptions FindOneAndReplaceOptions IndexOptions BulkWriteOptions DeleteManyModel DeleteOneModel InsertOneModel ReplaceOneModel UpdateManyModel UpdateOneModel)
(org.bson Document)
(java.util.concurrent TimeUnit)
(com.mongodb WriteConcern ReadPreference ReadConcern)
(clojure.lang Ratio Named IPersistentMap)
(java.util Collection List Date)
(org.bson.types Decimal128)))
(set! *warn-on-reflection* true)
;;; Conversions
(defprotocol ConvertToDocument
(^Document document [input] "Convert from clojure to Mongo Document"))
(defn read-dates-as-instants! []
(extend-protocol ConvertToDocument
Date
(document [input _]
(.toInstant ^Date input))))
(extend-protocol ConvertToDocument
nil
(document [_]
nil)
Ratio
(document [^Ratio input]
(double input))
Named
(document [^Named input]
(.getName input))
IPersistentMap
(document [^IPersistentMap input]
(reduce-kv
(fn [^Document doc k v]
(doto doc
(.append (document k) (document v))))
(Document.)
input))
Collection
(document [^Collection input]
(mapv document input))
Object
(document [input]
input))
(defprotocol ConvertFromDocument
(from-document [input keywordize?] "Converts Mongo 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?]
(mapv #(from-document % keywordize?) input))
Document
(from-document [^Document input keywordize?]
(persistent!
(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))))
(transient {})
(.keySet input)))))
;;; Config
(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. See `collection` for usage."
^ReadConcern [{:keys [read-concern]}]
(when read-concern
(if (instance? ReadConcern read-concern)
read-concern
(or (kw->ReadConcern read-concern)
(throw (IllegalArgumentException.
(str "No match for read concern of " (name read-concern))))))))
(defn ->ReadPreference
"Coerce `rp` into a ReadPreference if not nil. See `collection` for usage."
^ReadPreference [{:keys [read-preference]}]
(when read-preference
(if (instance? ReadPreference read-preference)
read-preference
(ReadPreference/valueOf (name read-preference)))))
(defn ->WriteConcern
"Coerces write-concern related options to a WriteConcern. See `collection` for usage."
^WriteConcern [{:keys [write-concern ^Integer write-concern/w ^Long write-concern/w-timeout-ms ^Boolean write-concern/journal?]}]
(when (some some? [write-concern w w-timeout-ms journal?])
(let [^WriteConcern wc (when write-concern
(if (instance? WriteConcern write-concern)
write-concern
(WriteConcern/valueOf (name write-concern))))]
(cond-> (or wc (WriteConcern/ACKNOWLEDGED))
w (.withW w)
w-timeout-ms (.withWTimeout w-timeout-ms (TimeUnit/MILLISECONDS))
(some? journal?) (.withJournal journal?)))))
(defn ->BulkWriteOptions
"Coerce options map into BulkWriteOptions. See `bulk-write` for usage."
^BulkWriteOptions [{:keys [bulk-write-options bypass-document-validation? ordered?]}]
(let [^BulkWriteOptions opts (or bulk-write-options (BulkWriteOptions.))]
(cond-> opts
(some? bypass-document-validation?) (.bypassDocumentValidation bypass-document-validation?)
(some? ordered?) (.ordered ordered?))))
(defn ->CountOptions
"Coerce options map into CountOptions. See `count-documents` for usage."
^CountOptions [{:keys [count-options hint limit max-time-ms skip]}]
(let [^CountOptions opts (or count-options (CountOptions.))]
(cond-> opts
hint (.hint (document hint))
limit (.limit limit)
max-time-ms (.maxTime max-time-ms (TimeUnit/MILLISECONDS))
skip (.skip skip))))
(defn ->DeleteOptions
"Coerce options map into DeleteOptions. See `delete-one` and `delete-many` for usage."
^DeleteOptions [{:keys [delete-options]}]
(let [^DeleteOptions opts (or delete-options (DeleteOptions.))]
opts))
(defn ->FindOneAndReplaceOptions
"Coerce options map into FindOneAndReplaceOptions. See `find-one-and-replace` for usage."
^FindOneAndReplaceOptions [{:keys [find-one-and-replace-options upsert? return-new? sort projection]}]
(let [^FindOneAndReplaceOptions opts (or find-one-and-replace-options (FindOneAndReplaceOptions.))]
(cond-> opts
(some? upsert?) (.upsert upsert?)
return-new? (.returnDocument (ReturnDocument/AFTER))
sort (.sort (document sort))
projection (.projection (document projection)))))
(defn ->FindOneAndUpdateOptions
"Coerce options map into FindOneAndUpdateOptions. See `find-one-and-update` for usage."
^FindOneAndUpdateOptions [{:keys [find-one-and-update-options upsert? return-new? sort projection]}]
(let [^FindOneAndUpdateOptions opts (or find-one-and-update-options (FindOneAndUpdateOptions.))]
(cond-> opts
(some? upsert?) (.upsert upsert?)
return-new? (.returnDocument (ReturnDocument/AFTER))
sort (.sort (document sort))
projection (.projection (document projection)))))
(defn ^IndexOptions ->IndexOptions
"Coerces an options map into an IndexOptions.
See `create-index` for usage"
[{:keys [index-options name sparse? unique?]}]
(let [^IndexOptions opts (or index-options (IndexOptions.))]
(cond-> opts
name (.name name)
(some? sparse?) (.sparse sparse?)
(some? unique?) (.unique unique?))))
(defn ->InsertManyOptions
"Coerce options map into InsertManyOptions. See `insert-many` for usage."
^InsertManyOptions [{:keys [insert-many-options bypass-document-validation? ordered?]}]
(let [^InsertManyOptions opts (or insert-many-options (InsertManyOptions.))]
(cond-> opts
(some? bypass-document-validation?) (.bypassDocumentValidation bypass-document-validation?)
(some? ordered?) (.ordered ordered?))))
(defn ->InsertOneOptions
"Coerce options map into InsertOneOptions. See `insert-one` for usage."
^InsertOneOptions [{:keys [insert-one-options bypass-document-validation?]}]
(let [^InsertOneOptions opts (or insert-one-options (InsertOneOptions.))]
(cond-> opts
(some? bypass-document-validation?) (.bypassDocumentValidation bypass-document-validation?))))
(defn ->ReplaceOptions
"Coerce options map into ReplaceOptions. See `replace-one` and `replace-many` for usage."
^ReplaceOptions [{:keys [replace-options upsert? bypass-document-validation?]}]
(let [^ReplaceOptions opts (or replace-options (ReplaceOptions.))]
(cond-> opts
(some? upsert?) (.upsert upsert?)
(some? bypass-document-validation?) (.bypassDocumentValidation bypass-document-validation?))))
(defn ->UpdateOptions
"Coerce options map into UpdateOptions. See `update-one` and `update-many` for usage."
^UpdateOptions [{:keys [update-options upsert? bypass-document-validation?]}]
(let [^UpdateOptions opts (or update-options (UpdateOptions.))]
(cond-> opts
(some? upsert?) (.upsert upsert?)
(some? bypass-document-validation?) (.bypassDocumentValidation bypass-document-validation?))))
(defn ->CreateCollectionOptions
"Coerce options map into CreateCollectionOptions. See `create` usage."
^CreateCollectionOptions [{:keys [create-collection-options capped? max-documents max-size-bytes]}]
(let [^CreateCollectionOptions opts (or create-collection-options (CreateCollectionOptions.))]
(cond-> opts
(some? capped?) (.capped capped?)
max-documents (.maxDocuments max-documents)
max-size-bytes (.sizeInBytes max-size-bytes))))
(defn ^RenameCollectionOptions ->RenameCollectionOptions
"Coerce options map into RenameCollectionOptions. See `rename` usage."
[{:keys [rename-collection-options drop-target?]}]
(let [^RenameCollectionOptions opts (or rename-collection-options (RenameCollectionOptions.))]
(cond-> opts
(some? drop-target?) (.dropTarget drop-target?))))
(defmulti write-model
(fn [[type _]] type))
(defmethod write-model :delete-many
[[_ opts]]
(DeleteManyModel. (document (:filter opts)) (->DeleteOptions opts)))
(defmethod write-model :delete-one
[[_ opts]]
(DeleteOneModel. (document (:filter opts)) (->DeleteOptions opts)))
(defmethod write-model :insert-one
[[_ opts]]
(InsertOneModel. (document (:document opts))))
(defmethod write-model :replace-one
[[_ opts]]
(ReplaceOneModel. (document (:filter opts)) (document (:replacement opts)) (->ReplaceOptions opts)))
(defmethod write-model :update-many
[[_ opts]]
(UpdateManyModel. (document (:filter opts)) (document (:update opts)) (->UpdateOptions opts)))
(defmethod write-model :update-one
[[_ opts]]
(UpdateOneModel. (document (:filter opts)) (document (:update opts)) (->UpdateOptions opts)))

View file

@ -1,135 +1,21 @@
(ns mongo-driver-3.client-test (ns mongo-driver-3.client-test
(:require [clojure.test :refer :all] (:require [clojure.test :refer :all]
[mongo-driver-3.client :as mg] [mongo-driver-3.client :as mg])
[mongo-driver-3.collection :as mc] (:import (com.mongodb.client MongoClient MongoDatabase)))
[mongo-driver-3.model :as m])
(:import (com.mongodb.client MongoClient MongoDatabase MongoIterable ListCollectionsIterable ClientSession)
(java.util UUID)
(com.mongodb ClientSessionOptions ReadConcern ReadPreference)
(java.util.concurrent TimeUnit)))
;;; Unit
(deftest test->ClientSessionOptions
(is (instance? ClientSessionOptions (mg/->ClientSessionOptions {})))
(are [expected arg]
(= expected (.isCausallyConsistent (mg/->ClientSessionOptions {:causally-consistent? arg})))
true true
false false
nil nil)
(is (= 7 (.getMaxCommitTime (.getDefaultTransactionOptions
(mg/->ClientSessionOptions {:max-commit-time-ms 7})) (TimeUnit/MILLISECONDS))))
(is (= (ReadConcern/AVAILABLE) (.getReadConcern (.getDefaultTransactionOptions
(mg/->ClientSessionOptions {:read-concern :available})))))
(is (= (ReadPreference/primary) (.getReadPreference (.getDefaultTransactionOptions (mg/->ClientSessionOptions {:read-preference :primary})))))
(is (nil? (.getWriteConcern (.getDefaultTransactionOptions (mg/->ClientSessionOptions {})))))
(is (= 1 (.getW (.getWriteConcern (.getDefaultTransactionOptions (mg/->ClientSessionOptions {:write-concern/w 1}))))))
(let [opts (.build (.causallyConsistent (ClientSessionOptions/builder) true))]
(is (= opts (mg/->ClientSessionOptions {:client-session-options opts})) "configure directly")))
;;; Integration ;;; Integration
; docker run -it --rm -p 27017:27017 mongo:4.2 ; docker run -it --rm -p 27017:27017 mongo:4.2
(def mongo-host "mongodb://localhost:27017")
(deftest ^:integration test-create (def mongo-host (or (System/getenv "MONGO_HOST") "mongodb://localhost:27017"))
(deftest test-create
(is (instance? MongoClient (mg/create))) (is (instance? MongoClient (mg/create)))
(is (instance? MongoClient (mg/create mongo-host)))) (is (instance? MongoClient (mg/create mongo-host))))
(deftest ^:integration test-connect-to-db (deftest test-connect-to-db
(is (thrown? IllegalArgumentException (mg/connect-to-db mongo-host))) (is (thrown? IllegalArgumentException (mg/connect-to-db mongo-host)))
(let [res (mg/connect-to-db (str mongo-host "/my-db"))] (let [res (mg/connect-to-db (str mongo-host "/my-db"))]
(is (instance? MongoClient (:client res))) (is (instance? MongoClient (:client res)))
(is (instance? MongoDatabase (:db res))) (is (instance? MongoDatabase (:db res)))
(is (= "my-db" (.getName (:db res)))))) (is (= "my-db" (.getName (:db res))))))
(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-list-collections
(let [db (new-db @client)
_ (mc/create db "test")]
(is (= ["test"] (map :name (mg/list-collections db))))
(is (= ["test"] (map #(get % "name") (mg/list-collections db {:keywordize? false}))))
(is (instance? ListCollectionsIterable (mg/list-collections db {:raw? true})))))
(deftest ^:integration test-list-collection-names
(let [db (new-db @client)
_ (mc/create db "test")]
(is (= ["test"] (mg/list-collection-names db)))
(is (instance? MongoIterable (mg/list-collection-names db {:raw? true})))))
(comment
;; Currently in a comment because it is troublesome to set up a replset in the CI
;; docker run -it --rm -p 27017:27017 mongo:4.2 --replSet rs1
;; Ensure we have a replica set so we can run session tests
(let [client (mg/create mongo-host)
admin-db (mg/get-db client "admin")]
(try (.runCommand admin-db (mc/document {:replSetInitiate {}}))
(catch Exception _ "already initialized")))
(deftest ^:integration test-start-session
(is (instance? ClientSession (mg/start-session @client))))
(deftest ^:integration test-with-transaction
(let [db (new-db @client)
_ (mc/create db "test")]
(with-open [session (mg/start-session @client)]
(is (= 2 (mg/with-transaction session
(fn []
(mc/insert-one db "test" {:a 1} {:session session})
(mc/insert-one db "test" {:a 2} {:session session})
(mc/count-documents db "test" {} {:session session}))))))))
(deftest ^:integration test-with-implicit-transaction
(testing "passing"
(let [db (new-db @client)
_ (mc/create db "test")]
(is (= 2 (mg/with-implicit-transaction
{:client @client}
(fn []
(mc/insert-one db "test" {:a 1})
(mc/insert-one db "test" {:a 2})
(mc/count-documents db "test" {})))))))
(testing "failing"
(let [db (new-db @client)
_ (mc/create db "test")]
(is (= 0 (try (mg/with-implicit-transaction
{:client @client}
(fn []
(mc/insert-one db "test" {:a 1})
(mc/insert-one db "test" {nil 2})))
(catch Exception _ (mc/count-documents db "test" {}))))))))
(deftest ^:integration test-with-transaction
(testing "passing"
(let [db (new-db @client)
_ (mc/create db "test")]
(with-open [session (mg/start-session @client)]
(is (= 2 (mg/with-transaction session
(fn []
(mc/insert-one db "test" {:a 1} {:session session})
(mc/insert-one db "test" {:a 2} {:session session})
(mc/count-documents db "test" {} {:session session}))))))))
(testing "failing"
(let [db (new-db @client)
_ (mc/create db "test")]
(with-open [session (mg/start-session @client)]
(is (= 0 (try (mg/with-transaction session
(fn []
(mc/insert-one db "test" {:a 1} {:session session})
(mc/insert-one db "test" {nil 2} {:session session})))
(catch Exception _ (mc/count-documents db "test" {}))))))))))

View file

@ -2,10 +2,186 @@
(:require [clojure.test :refer :all] (:require [clojure.test :refer :all]
[mongo-driver-3.client :as mg] [mongo-driver-3.client :as mg]
[mongo-driver-3.collection :as mc]) [mongo-driver-3.collection :as mc])
(:import (java.time ZoneId LocalDate LocalTime LocalDateTime) (: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 RenameCollectionOptions)
(java.time ZoneId LocalDate LocalTime LocalDateTime)
(java.util Date UUID) (java.util Date UUID)
(com.mongodb.client FindIterable))) (com.mongodb.client FindIterable)))
;;; 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->RenameCollectionOptions
(is (instance? RenameCollectionOptions (mc/->RenameCollectionOptions {})))
(are [expected arg]
(= expected (.isDropTarget (mc/->RenameCollectionOptions {:drop-target? arg})))
true true
false false
false nil)
(let [opts (RenameCollectionOptions.)]
(is (= opts (mc/->RenameCollectionOptions {:rename-collection-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 ;;; Integration
; docker run -it --rm -p 27017:27017 mongo:4.2 ; docker run -it --rm -p 27017:27017 mongo:4.2
@ -144,11 +320,7 @@
(is (instance? FindIterable (mc/find db "test" {} {:raw? true})))) (is (instance? FindIterable (mc/find db "test" {} {:raw? true}))))
(testing "keywordize" (testing "keywordize"
(is (= [{"id" 1}] (mc/find db "test" {} {:keywordize? false :projection {:_id 0 :id 1} :limit 1})))) (is (= [{"id" 1}] (mc/find db "test" {} {:keywordize? false :projection {:_id 0 :id 1} :limit 1}))))))
(testing "realise-fn"
(is (seq? (mc/find db "test" {})))
(is (vector? (mc/find db "test" {} {:realise-fn (partial into [])}))))))
(deftest ^:integration test-find-one (deftest ^:integration test-find-one
(let [db (new-db @client) (let [db (new-db @client)
@ -220,13 +392,7 @@
(let [db (new-db @client)] (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 (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))))) (is (= {:id 1 :r 1} (dissoc (mc/find-one-and-update db "test" {:id 1} {:$set {:r 1}} {:return-new? true :upsert? true}) :_id))))))
(testing "aggregation pipeline"
(let [db (new-db @client)
_ (mc/insert-many db "test" [{:id 1 :a [1 2] :b []} {:id 2 :a [7 8] :b []}])]
(is (= {:id 1 :a [] :b [1 2]} (dissoc (mc/find-one-and-update db "test" {:id 1} [{:$set {:b :$a}} {:$set {:a []}}] {:return-new? true}) :_id))))))
(deftest ^:integration test-find-one-and-replace (deftest ^:integration test-find-one-and-replace
(testing "return new" (testing "return new"
@ -242,28 +408,6 @@
(is (nil? (dissoc (mc/find-one-and-replace db "test" {:id 1} {:id 1 :v 2} {:return-new? true}) :_id))) (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)))))) (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-bulk-write
(testing "existing docs"
(let [db (new-db @client)
_ (mc/insert-many db "test" [{:id 1} {:id 2} {:id 3} {:id 4}])
_ (mc/bulk-write db "test" [[:replace-one {:filter {:id 2} :replacement {:id 2.1}}]
[:update-many {:filter {:id 3} :update {:$set {:a "b"}}}]
[:update-one {:filter {:id 4} :update {:$set {:a "b"}}}]])]
(is (= [{:id 1} {:id 2.1} {:id 3 :a "b"} {:id 4 :a "b"}]
(mc/find db "test" {} {:projection {:_id 0}})))))
(testing "upsert"
(let [db (new-db @client)
res (mc/bulk-write db "test" [[:insert-one {:document {:id 1}}]
[:replace-one {:filter {:id 2} :replacement {:id 2.1} :upsert? true}]
[:update-many {:filter {:id 3} :update {:$set {:a "b"}} :upsert? true}]
[:update-one {:filter {:id 4} :update {:$set {:a "b"}} :upsert? true}]])]
(is (= 4 (mc/count-documents db "test")))
(is (= 1 (.getInsertedCount res)))
(is (= 3 (count (.getUpserts res)))))))
(deftest ^:integration test-replace-one (deftest ^:integration test-replace-one
(testing "existing doc" (testing "existing doc"
(let [db (new-db @client) (let [db (new-db @client)
@ -353,7 +497,12 @@
(testing "not existing" (testing "not existing"
(let [db (new-db @client) (let [db (new-db @client)
_ (mc/create db "my-coll")] _ (mc/create db "my-coll")]
(is (true? (coll-exists? db "my-coll")))))) (is (true? (coll-exists? db "my-coll")))))
(testing "existing"
(let [db (new-db @client)
_ (mc/create db "my-coll")]
(is (thrown? Exception (mc/create db "my-coll"))))))
(deftest ^:integration test-rename (deftest ^:integration test-rename
(testing "not existing" (testing "not existing"
@ -386,11 +535,7 @@
(deftest ^:integration test-list-indexes (deftest ^:integration test-list-indexes
(let [db (new-db @client) (let [db (new-db @client)
_ (mc/create db "test")] _ (mc/create db "test")]
(is (= 1 (count (mc/list-indexes db "test"))) "has default index") (is (= 1 (count (mc/list-indexes db "test"))) "has default index")))
(testing "realise-fn"
(is (seq? (mc/list-indexes db "test")))
(is (vector? (mc/list-indexes db "test" {:realise-fn (partial into [])}))))))
(deftest ^:integration test-create-index (deftest ^:integration test-create-index
(let [db (new-db @client) (let [db (new-db @client)

View file

@ -1,235 +0,0 @@
(ns mongo-driver-3.model-test
(:require [clojure.test :refer :all]
[mongo-driver-3.model :as m])
(: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 RenameCollectionOptions BulkWriteOptions DeleteManyModel DeleteOneModel InsertOneModel ReplaceOneModel UpdateManyModel UpdateOneModel)))
;;; Unit
(deftest test->ReadConcern
(is (nil? (m/->ReadConcern {})))
(is (thrown? IllegalArgumentException (m/->ReadConcern {:read-concern "invalid"})))
(is (instance? ReadConcern (m/->ReadConcern {:read-concern :available}))))
(deftest test->ReadPreference
(is (nil? (m/->ReadPreference {})))
(is (thrown? IllegalArgumentException (m/->ReadPreference {:read-preference "invalid"})))
(is (instance? ReadPreference (m/->ReadPreference {:read-preference :primary}))))
(deftest test->WriteConcern
(is (= (WriteConcern/W1) (m/->WriteConcern {:write-concern :w1})) "accepts kw")
(is (= (WriteConcern/W1) (m/->WriteConcern {:write-concern (WriteConcern/W1)})) "accepts WriteConcern")
(is (= (WriteConcern/ACKNOWLEDGED) (m/->WriteConcern {:write-concern "invalid"})) "defaults to acknowledged")
(is (= 1 (.getW (m/->WriteConcern {:write-concern/w 1}))) "set w")
(is (= 2 (.getW (m/->WriteConcern {:write-concern (WriteConcern/W2)}))))
(is (= 1 (.getW (m/->WriteConcern {:write-concern (WriteConcern/W2) :write-concern/w 1}))) "prefer granular option")
(is (true? (.getJournal (m/->WriteConcern {:write-concern/journal? true}))) "can set journal")
(is (= 77 (.getWTimeout (m/->WriteConcern {:write-concern/w-timeout-ms 77}) (TimeUnit/MILLISECONDS))) "can set timeout"))
(deftest test->InsertOneOptions
(is (instance? InsertOneOptions (m/->InsertOneOptions {})))
(is (true? (.getBypassDocumentValidation (m/->InsertOneOptions {:bypass-document-validation? true}))))
(is (true? (.getBypassDocumentValidation (m/->InsertOneOptions
{:insert-one-options (.bypassDocumentValidation (InsertOneOptions.) true)})))
"configure directly")
(is (false? (.getBypassDocumentValidation (m/->InsertOneOptions
{:insert-one-options (.bypassDocumentValidation (InsertOneOptions.) true)
:bypass-document-validation? false})))
"can override"))
(deftest test->ReplaceOptions
(is (instance? ReplaceOptions (m/->ReplaceOptions {})))
(are [expected arg]
(= expected (.isUpsert (m/->ReplaceOptions {:upsert? arg})))
true true
false false
false nil)
(is (true? (.getBypassDocumentValidation (m/->ReplaceOptions {:bypass-document-validation? true}))))
(is (true? (.getBypassDocumentValidation (m/->ReplaceOptions
{:replace-options (.bypassDocumentValidation (ReplaceOptions.) true)})))
"configure directly")
(is (false? (.getBypassDocumentValidation (m/->ReplaceOptions
{:replace-options (.bypassDocumentValidation (ReplaceOptions.) true)
:bypass-document-validation? false})))
"can override"))
(deftest test->UpdateOptions
(is (instance? UpdateOptions (m/->UpdateOptions {})))
(are [expected arg]
(= expected (.isUpsert (m/->UpdateOptions {:upsert? arg})))
true true
false false
false nil)
(is (true? (.getBypassDocumentValidation (m/->UpdateOptions {:bypass-document-validation? true}))))
(is (true? (.getBypassDocumentValidation (m/->UpdateOptions
{:update-options (.bypassDocumentValidation (UpdateOptions.) true)})))
"configure directly")
(is (false? (.getBypassDocumentValidation (m/->UpdateOptions
{:update-options (.bypassDocumentValidation (UpdateOptions.) true)
:bypass-document-validation? false})))
"can override"))
(deftest test->InsertManyOptions
(is (instance? InsertManyOptions (m/->InsertManyOptions {})))
(are [expected arg]
(= expected (.getBypassDocumentValidation (m/->InsertManyOptions {:bypass-document-validation? arg})))
true true
false false
nil nil)
(are [expected arg]
(= expected (.isOrdered (m/->InsertManyOptions {:ordered? arg})))
true true
false false
true nil)
(is (true? (.getBypassDocumentValidation (m/->InsertManyOptions
{:insert-many-options (.bypassDocumentValidation (InsertManyOptions.) true)})))
"configure directly")
(is (false? (.getBypassDocumentValidation (m/->InsertManyOptions
{:insert-many-options (.bypassDocumentValidation (InsertManyOptions.) true)
:bypass-document-validation? false})))
"can override"))
(deftest test->DeleteOptions
(is (instance? DeleteOptions (m/->DeleteOptions {})))
(let [opts (DeleteOptions.)]
(is (= opts (m/->DeleteOptions {:delete-options opts})) "configure directly")))
(deftest test->RenameCollectionOptions
(is (instance? RenameCollectionOptions (m/->RenameCollectionOptions {})))
(are [expected arg]
(= expected (.isDropTarget (m/->RenameCollectionOptions {:drop-target? arg})))
true true
false false
false nil)
(let [opts (RenameCollectionOptions.)]
(is (= opts (m/->RenameCollectionOptions {:rename-collection-options opts})) "configure directly")))
(deftest test->FindOneAndUpdateOptions
(is (instance? FindOneAndUpdateOptions (m/->FindOneAndUpdateOptions {})))
(let [opts (FindOneAndUpdateOptions.)]
(is (= opts (m/->FindOneAndUpdateOptions {:find-one-and-update-options opts})) "configure directly"))
(are [expected arg]
(= expected (.isUpsert (m/->FindOneAndUpdateOptions {:upsert? arg})))
true true
false false
false nil)
(are [expected arg]
(= expected (.getReturnDocument (m/->FindOneAndUpdateOptions {:return-new? arg})))
(ReturnDocument/AFTER) true
(ReturnDocument/BEFORE) false
(ReturnDocument/BEFORE) nil)
(is (= {"_id" 1} (.getSort (m/->FindOneAndUpdateOptions {:sort {:_id 1}}))))
(is (= {"_id" 1} (.getProjection (m/->FindOneAndUpdateOptions {:projection {:_id 1}})))))
(deftest test->FindOneAndReplaceOptions
(is (instance? FindOneAndReplaceOptions (m/->FindOneAndReplaceOptions {})))
(let [opts (FindOneAndReplaceOptions.)]
(is (= opts (m/->FindOneAndReplaceOptions {:find-one-and-replace-options opts})) "configure directly"))
(are [expected arg]
(= expected (.isUpsert (m/->FindOneAndReplaceOptions {:upsert? arg})))
true true
false false
false nil)
(are [expected arg]
(= expected (.getReturnDocument (m/->FindOneAndReplaceOptions {:return-new? arg})))
(ReturnDocument/AFTER) true
(ReturnDocument/BEFORE) false
(ReturnDocument/BEFORE) nil)
(is (= {"_id" 1} (.getSort (m/->FindOneAndReplaceOptions {:sort {:_id 1}}))))
(is (= {"_id" 1} (.getProjection (m/->FindOneAndReplaceOptions {:projection {:_id 1}})))))
(deftest test->CountOptions
(is (instance? CountOptions (m/->CountOptions {})))
(let [opts (CountOptions.)]
(is (= opts (m/->CountOptions {:count-options opts})) "configure directly"))
(is (= {"a" 1} (.getHint (m/->CountOptions {:hint {:a 1}}))))
(is (= 7 (.getLimit (m/->CountOptions {:limit 7}))))
(is (= 2 (.getSkip (m/->CountOptions {:skip 2}))))
(is (= 42 (.getMaxTime (m/->CountOptions {:max-time-ms 42}) (TimeUnit/MILLISECONDS)))))
(deftest test->IndexOptions
(is (instance? IndexOptions (m/->IndexOptions {})))
(are [expected arg]
(= expected (.isSparse (m/->IndexOptions {:sparse? arg})))
true true
false false
false nil)
(are [expected arg]
(= expected (.isUnique (m/->IndexOptions {:unique? arg})))
true true
false false
false nil)
(let [opts (IndexOptions.)]
(is (= opts (m/->IndexOptions {:index-options opts})) "configure directly")))
(deftest test->CreateCollectionOptions
(are [expected arg]
(= expected (.isCapped (m/->CreateCollectionOptions {:capped? arg})))
true true
false false
false nil)
(is (= 7 (.getMaxDocuments (m/->CreateCollectionOptions {:max-documents 7}))))
(is (= 42 (.getSizeInBytes (m/->CreateCollectionOptions {:max-size-bytes 42}))))
(let [opts (-> (CreateCollectionOptions.) (.maxDocuments 5))]
(is (= opts (m/->CreateCollectionOptions {:create-collection-options opts})) "configure directly")
(is (= 5 (.getMaxDocuments (m/->CreateCollectionOptions {:create-collection-options opts}))))
(is (= 7 (.getMaxDocuments (m/->CreateCollectionOptions {:create-collection-options opts :max-documents 7})))
"can override")))
(deftest test->BulkWriteOptions
(is (instance? BulkWriteOptions (m/->BulkWriteOptions {})))
(are [expected arg]
(= expected (.getBypassDocumentValidation (m/->BulkWriteOptions {:bypass-document-validation? arg})))
true true
false false
nil nil)
(are [expected arg]
(= expected (.isOrdered (m/->BulkWriteOptions {:ordered? arg})))
true true
false false
true nil)
(is (true? (.getBypassDocumentValidation (m/->BulkWriteOptions
{:bulk-write-options (.bypassDocumentValidation (BulkWriteOptions.) true)})))
"configure directly")
(is (false? (.getBypassDocumentValidation (m/->BulkWriteOptions
{:bulk-write-options (.bypassDocumentValidation (BulkWriteOptions.) true)
:bypass-document-validation? false})))
"can override"))
(deftest test-write-model
(testing "delete many"
(is (instance? DeleteManyModel (m/write-model [:delete-many {:filter {:a "b"}}]))))
(testing "delete one"
(is (instance? DeleteOneModel (m/write-model [:delete-one {:filter {:a "b"}}]))))
(testing "insert one"
(is (instance? InsertOneModel (m/write-model [:insert-one {:document {:a "b"}}]))))
(testing "replace one"
(is (instance? ReplaceOneModel (m/write-model [:replace-one {:filter {:a "b"} :replacement {:a "c"}}])))
(are [expected arg]
(= expected (.isUpsert (.getReplaceOptions (m/write-model [:replace-one {:filter {:a "b"} :replacement {:a "c"} :upsert? arg}]))))
true true
false false
false nil))
(testing "update many"
(is (instance? UpdateManyModel (m/write-model [:update-many {:filter {:a "b"} :update {"$set" {:a "c"}}}])))
(are [expected arg]
(= expected (.isUpsert (.getOptions ^UpdateManyModel (m/write-model [:update-many {:filter {:a "b"} :update {"$set" {:a "c"}} :upsert? arg}]))))
true true
false false
false nil))
(testing "update one"
(is (instance? UpdateOneModel (m/write-model [:update-one {:filter {:a "b"} :update {"$set" {:a "c"}}}])))
(are [expected arg]
(= expected (.isUpsert (.getOptions (m/write-model [:update-one {:filter {:a "b"} :update {"$set" {:a "c"}} :upsert? arg}]))))
true true
false false
false nil)))