diff --git a/.gitignore b/.gitignore index c53038e..18ec56e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ pom.xml.asc /.nrepl-port .hgignore .hg/ +.lsp/ +.clj-kondo/ diff --git a/src/mongo_driver_3/client.clj b/src/mongo_driver_3/client.clj index 747ba45..f3f2b1d 100644 --- a/src/mongo_driver_3/client.clj +++ b/src/mongo_driver_3/client.clj @@ -1,6 +1,7 @@ (ns mongo-driver-3.client (:refer-clojure :exclude [find]) - (:require [mongo-driver-3.model :as m]) + (:require [mongo-driver-3.model :as m] + [mongo-driver-3.iterable :as iterable]) (:import (com.mongodb.client MongoClients MongoClient ClientSession MongoDatabase TransactionBody) (com.mongodb ConnectionString ClientSessionOptions TransactionOptions) (java.util.concurrent TimeUnit))) @@ -15,16 +16,15 @@ `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))) + (^MongoClient [] (MongoClients/create)) + (^MongoClient [^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] + ^MongoDatabase [^MongoClient client ^String name] (.getDatabase client name)) (defn close @@ -41,16 +41,20 @@ - `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 seq, default: 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] :or {keywordize? true}}] + ([^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-not raw? - (map #(m/from-document % keywordize?) (seq it)) - it)))) + (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. @@ -66,9 +70,9 @@ (let [it (if-let [^ClientSession session (:session opts)] (.listCollectionNames db session) (.listCollectionNames db))] - (if-not (:raw? opts) - (seq it) - it)))) + (if (:raw? opts) + it + (seq it))))) (defn ->TransactionOptions "Coerces options map into a TransactionOptions. See `start-session` for usage." diff --git a/src/mongo_driver_3/collection.clj b/src/mongo_driver_3/collection.clj index e9c1518..25a24fe 100644 --- a/src/mongo_driver_3/collection.clj +++ b/src/mongo_driver_3/collection.clj @@ -1,7 +1,8 @@ (ns mongo-driver-3.collection (:refer-clojure :exclude [find empty? drop]) (:require [mongo-driver-3.model :refer :all] - [mongo-driver-3.client :refer [*session*]]) + [mongo-driver-3.client :refer [*session*]] + [mongo-driver-3.iterable :as iterable]) (:import (com.mongodb MongoNamespace) (com.mongodb.client MongoDatabase MongoCollection ClientSession) (com.mongodb.client.model IndexModel) @@ -62,23 +63,27 @@ - `:batch-size` Documents to return per batch, e.g. 1 - `:bypass-document-validation?` Boolean - `: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 - `:session` a ClientSession" ([^MongoDatabase db coll pipeline] (aggregate db coll pipeline {})) ([^MongoDatabase db coll pipeline opts] - (let [{:keys [^ClientSession session allow-disk-use? ^Integer batch-size bypass-document-validation? keywordize? raw?] :or {keywordize? true raw? false}} opts + (let [{:keys [^ClientSession session allow-disk-use? ^Integer batch-size bypass-document-validation? keywordize? raw? realise-fn] + :or {keywordize? true + realise-fn sequence}} opts ^ClientSession session (or session *session*) it (cond-> (if session (.aggregate (collection db coll opts) session ^List (map document pipeline)) (.aggregate (collection db coll opts) ^List (map document pipeline))) - (some? allow-disk-use?) (.allowDiskUse allow-disk-use?) - (some? bypass-document-validation?) (.bypassDocumentValidation bypass-document-validation?) - batch-size (.batchSize batch-size))] + (some? allow-disk-use?) (.allowDiskUse allow-disk-use?) + (some? bypass-document-validation?) (.bypassDocumentValidation bypass-document-validation?) + batch-size (.batchSize batch-size))] - (if-not raw? - (map (fn [x] (from-document x keywordize?)) (seq it)) - it)))) + (if raw? + it + (realise-fn ;; accomodate users who don't want to use lazy-seqs + (iterable/documents it keywordize?)))))) (defn bulk-write "Executes a mix of inserts, updates, replaces, and deletes. @@ -190,26 +195,30 @@ - `:sort` document representing sort order, e.g. {:timestamp -1} - `:projection` document representing fields to return, e.g. {:_id 0} - `: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 - `:session` a ClientSession Additionally takes options specified in `collection`." ([^MongoDatabase db coll q] (find db coll q {})) - ([^MongoDatabase db coll q opts] - (let [{:keys [limit skip sort projection ^ClientSession session keywordize? raw?] :or {keywordize? true raw? false}} opts] - (let [^ClientSession session (or session *session*) - it (cond-> (if session - (.find (collection db coll opts) session (document q)) - (.find (collection db coll opts) (document q))) - limit (.limit limit) - skip (.skip skip) - sort (.sort (document sort)) - projection (.projection (document projection)))] + ([^MongoDatabase db coll q {:keys [limit skip sort projection ^ClientSession session keywordize? raw? realise-fn] + :or {keywordize? true + realise-fn sequence} + :as opts}] + (let [^ClientSession session (or session *session*) + it (cond-> (if session + (.find (collection db coll opts) session (document q)) + (.find (collection db coll opts) (document q))) + limit (.limit limit) + skip (.skip skip) + sort (.sort (document sort)) + projection (.projection (document projection)))] - (if-not raw? - (map (fn [x] (from-document x keywordize?)) (seq it)) - it))))) + (if raw? + it + (realise-fn ;; accomodate users who don't want to use lazy-seqs + (iterable/documents it keywordize?)))))) (defn find-one "Finds a single document and returns it as a clojure map, or nil if not found. @@ -493,7 +502,7 @@ (create-indexes db coll indexes {})) ([^MongoDatabase db coll indexes opts] (->> indexes - (map (fn [x] (IndexModel. (document (:keys x)) (->IndexOptions x)))) + (mapv (fn [x] (IndexModel. (document (:keys x)) (->IndexOptions x)))) (.createIndexes (collection db coll opts))))) (defn list-indexes @@ -501,5 +510,7 @@ ([^MongoDatabase db coll] (list-indexes db coll {})) ([^MongoDatabase db coll opts] - (->> (.listIndexes (collection db coll opts)) - (map #(from-document % true))))) \ No newline at end of file + (let [it (.listIndexes (collection db coll opts)) + realise-fn (:realise-fn opts sequence)] + (realise-fn + (iterable/documents it true))))) \ No newline at end of file diff --git a/src/mongo_driver_3/data_literals.clj b/src/mongo_driver_3/data_literals.clj index a28b4ae..9367f11 100644 --- a/src/mongo_driver_3/data_literals.clj +++ b/src/mongo_driver_3/data_literals.clj @@ -1,10 +1,36 @@ (ns mongo-driver-3.data-literals (:import (org.bson.types ObjectId) - (java.io Writer))) + (java.io Writer) + (java.util Date) + (java.nio ByteBuffer))) -(defmethod print-method ObjectId [c ^Writer w] (.write w ^String (str "#mongo/id \"" (.toHexString c) "\""))) -(defmethod print-dup ObjectId [c ^Writer w] (.write w ^String (str "#mongo/id \"" (.toHexString c) "\""))) +(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) \"))) -(defn mongo-id [o] - (ObjectId. o)) +(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))))) diff --git a/src/mongo_driver_3/iterable.clj b/src/mongo_driver_3/iterable.clj new file mode 100644 index 0000000..650bbd2 --- /dev/null +++ b/src/mongo_driver_3/iterable.clj @@ -0,0 +1,8 @@ +(ns mongo-driver-3.iterable + (:require [mongo-driver-3.model :as m])) + +(defn documents + "Given a MongoIterable , returns an eduction which will + eventually yield all the documents (per `m/from-document`)." + [it keywordize?] + (eduction (map #(m/from-document % keywordize?)) it)) diff --git a/src/mongo_driver_3/model.clj b/src/mongo_driver_3/model.clj index f6e3bd8..e5dac25 100644 --- a/src/mongo_driver_3/model.clj +++ b/src/mongo_driver_3/model.clj @@ -3,7 +3,7 @@ (org.bson Document) (java.util.concurrent TimeUnit) (com.mongodb WriteConcern ReadPreference ReadConcern) - (clojure.lang Ratio Keyword Named IPersistentMap) + (clojure.lang Ratio Named IPersistentMap) (java.util Collection List Date) (org.bson.types Decimal128))) @@ -16,8 +16,8 @@ (defn read-dates-as-instants! [] (extend-protocol ConvertToDocument - Date - (from-document [input _] + Date + (document [input _] (.toInstant ^Date input)))) (extend-protocol ConvertToDocument @@ -29,24 +29,22 @@ (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)) + (reduce-kv + (fn [^Document doc k v] + (doto doc + (.append (document k) (document v)))) + (Document.) + input)) Collection (document [^Collection input] - (map document input)) + (mapv document input)) Object (document [input] @@ -69,16 +67,18 @@ List (from-document [^List input keywordize?] - (vec (map #(from-document % keywordize?) input))) + (mapv #(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)))) + (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 @@ -92,24 +92,25 @@ (defn ->ReadConcern "Coerce `rc` into a ReadConcern if not nil. See `collection` for usage." - [{:keys [read-concern]}] + ^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)))))))) + (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." - [{:keys [read-preference]}] + ^ReadPreference [{:keys [read-preference]}] (when read-preference (if (instance? ReadPreference read-preference) read-preference (ReadPreference/valueOf (name read-preference))))) -(defn ^WriteConcern ->WriteConcern +(defn ->WriteConcern "Coerces write-concern related options to a WriteConcern. See `collection` for usage." - [{:keys [write-concern ^Integer write-concern/w ^Long write-concern/w-timeout-ms ^Boolean write-concern/journal?]}] + ^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) @@ -120,17 +121,17 @@ w-timeout-ms (.withWTimeout w-timeout-ms (TimeUnit/MILLISECONDS)) (some? journal?) (.withJournal journal?))))) -(defn ^BulkWriteOptions ->BulkWriteOptions +(defn ->BulkWriteOptions "Coerce options map into BulkWriteOptions. See `bulk-write` for usage." - [{:keys [bulk-write-options bypass-document-validation? ordered?]}] + ^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 ->CountOptions +(defn ->CountOptions "Coerce options map into CountOptions. See `count-documents` for usage." - [{:keys [count-options hint limit max-time-ms skip]}] + ^CountOptions [{:keys [count-options hint limit max-time-ms skip]}] (let [^CountOptions opts (or count-options (CountOptions.))] (cond-> opts hint (.hint (document hint)) @@ -138,15 +139,15 @@ max-time-ms (.maxTime max-time-ms (TimeUnit/MILLISECONDS)) skip (.skip skip)))) -(defn ^DeleteOptions ->DeleteOptions +(defn ->DeleteOptions "Coerce options map into DeleteOptions. See `delete-one` and `delete-many` for usage." - [{:keys [delete-options]}] + ^DeleteOptions [{:keys [delete-options]}] (let [^DeleteOptions opts (or delete-options (DeleteOptions.))] opts)) -(defn ^FindOneAndReplaceOptions ->FindOneAndReplaceOptions +(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]}] + ^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?) @@ -154,9 +155,9 @@ sort (.sort (document sort)) projection (.projection (document projection))))) -(defn ^FindOneAndUpdateOptions ->FindOneAndUpdateOptions +(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]}] + ^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?) @@ -175,40 +176,40 @@ (some? sparse?) (.sparse sparse?) (some? unique?) (.unique unique?)))) -(defn ^InsertManyOptions ->InsertManyOptions +(defn ->InsertManyOptions "Coerce options map into InsertManyOptions. See `insert-many` for usage." - [{:keys [insert-many-options bypass-document-validation? ordered?]}] + ^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 ->InsertOneOptions +(defn ->InsertOneOptions "Coerce options map into InsertOneOptions. See `insert-one` for usage." - [{:keys [insert-one-options bypass-document-validation?]}] + ^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 ->ReplaceOptions +(defn ->ReplaceOptions "Coerce options map into ReplaceOptions. See `replace-one` and `replace-many` for usage." - [{:keys [replace-options upsert? bypass-document-validation?]}] + ^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 ->UpdateOptions +(defn ->UpdateOptions "Coerce options map into UpdateOptions. See `update-one` and `update-many` for usage." - [{:keys [update-options upsert? bypass-document-validation?]}] + ^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 ->CreateCollectionOptions +(defn ->CreateCollectionOptions "Coerce options map into CreateCollectionOptions. See `create` usage." - [{:keys [create-collection-options capped? max-documents max-size-bytes]}] + ^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?)