diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d87d4..a90a780 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # 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/). +## [Unreleased] +### Added +- list collections +- start session + ## [0.3.1] ### Added - More documentation diff --git a/README.md b/README.md index 7f80854..61c8b16 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,15 @@ For Leinengen, add this to your project.clj: ## 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. `connect-to-db` is a convenience function that allows you to do this directly. ```clojure -(ns my.app - (:require [mongo-driver-3.client :as mcl])) - (mcl/connect-to-db "mongodb://localhost:27017/my-db") ; => ; { @@ -55,7 +57,7 @@ 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 manually: +You can also create a client and get a DB separately: ```clojure ;; Calling create without an arg will try and connect to the default host/port. @@ -105,7 +107,7 @@ As an example: ``` While most options are supported directly, sometimes you may need to some extra control. -In such cases, you can pass in a configured java options object as option. Any other +In such cases, you can pass in a configured java options object. Any other options will be applied on top of this object. ```clojure diff --git a/src/mongo_driver_3/client.clj b/src/mongo_driver_3/client.clj index 90e2826..70f363e 100644 --- a/src/mongo_driver_3/client.clj +++ b/src/mongo_driver_3/client.clj @@ -1,7 +1,9 @@ (ns mongo-driver-3.client (:refer-clojure :exclude [find]) + (:require [mongo-driver-3.collection :as mc]) (:import (com.mongodb.client MongoClients MongoClient) - (com.mongodb ConnectionString))) + (com.mongodb ConnectionString ClientSessionOptions TransactionOptions) + (java.util.concurrent TimeUnit))) ;;; Core @@ -28,6 +30,93 @@ [^MongoClient client] (.close client)) +(defn ->TransactionOptions + "Coerces options map into a TransactionOptions." + [{:keys [read-concern read-preference max-commit-time-ms] :as opts}] + (-> (TransactionOptions/builder) + (#(if max-commit-time-ms (.maxCommitTime % max-commit-time-ms (TimeUnit/MILLISECONDS)) %)) + (#(if-let [rp (mc/->ReadPreference read-preference)] (.readPreference % rp) %)) + (#(if-let [rc (mc/->ReadConcern read-concern)] (.readConcern % rc) %)) + (#(if-let [wc (mc/->WriteConcern opts)] (.writeConcern % wc) %)) + (.build))) + +(defn ->ClientSessionOptions + "Coerces an options map into a ClientSessionOptions. + + See `start-session` for usage" + [{:keys [client-session-options causally-consistent?] :as opts}] + (let [trans-opts (->TransactionOptions opts)] + (-> (if client-session-options (ClientSessionOptions/builder client-session-options) (ClientSessionOptions/builder)) + (.defaultTransactionOptions trans-opts) + (#(if (some? causally-consistent?) (.causallyConsistent % causally-consistent?) %)) + (.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." + ([client] (start-session client {})) + ([client opts] + (.startSession client (->ClientSessionOptions opts)))) + +(defn 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 seq, default: false + - `:session` a ClientSession" + ([db] (collections db {})) + ([db {:keys [raw? keywordize? session] :or {keywordize? true}}] + (let [it (if session + (.listCollections db session) + (.listCollections db))] + (if-not raw? + (map #(mc/from-document % keywordize?) (seq it)) + it)))) + +(defn 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" + ([db] (collection-names db {})) + ([db opts] + (let [it (if-let [session (:session opts)] + (.listCollectionNames db session) + (.listCollectionNames db))] + (if-not (:raw? opts) + (seq it) + it)))) + ;;; Utility (defn connect-to-db diff --git a/test/mongo_driver_3/client_test.clj b/test/mongo_driver_3/client_test.clj index 96a4049..86a0929 100644 --- a/test/mongo_driver_3/client_test.clj +++ b/test/mongo_driver_3/client_test.clj @@ -1,21 +1,77 @@ (ns mongo-driver-3.client-test (:require [clojure.test :refer :all] - [mongo-driver-3.client :as mg]) - (:import (com.mongodb.client MongoClient MongoDatabase))) + [mongo-driver-3.client :as mg] + [mongo-driver-3.collection :as mc]) + (: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 -; docker run -it --rm -p 27017:27017 mongo:4.2 +; docker run -it --rm -p 27017:27017 mongo:4.2 --replset rs1 -(def mongo-host (or (System/getenv "MONGO_HOST") "mongodb://localhost:27017")) +(def mongo-host "mongodb://localhost:27017") -(deftest test-create +(deftest ^:integration test-create (is (instance? MongoClient (mg/create))) (is (instance? MongoClient (mg/create mongo-host)))) -(deftest test-connect-to-db +(deftest ^:integration test-connect-to-db (is (thrown? IllegalArgumentException (mg/connect-to-db mongo-host))) (let [res (mg/connect-to-db (str mongo-host "/my-db"))] (is (instance? MongoClient (:client res))) (is (instance? MongoDatabase (:db res))) - (is (= "my-db" (.getName (:db res)))))) \ No newline at end of file + (is (= "my-db" (.getName (:db res)))))) + +(def client (atom nil)) + +(defn- setup-connections [f] + (reset! client (mg/create mongo-host)) + ;; Ensure we have a replica set so we can run session tests + (let [admin-db (mg/get-db @client "admin")] + (try (.runCommand admin-db (mc/document {:replSetInitiate {}})) + (catch Exception _ "already initialized"))) + (f) + (mg/close @client)) + +(use-fixtures :once setup-connections) + +(defn new-db + [client] + (mg/get-db client (.toString (UUID/randomUUID)))) + +(deftest ^:integration test-collection-names + (let [db (new-db @client) + _ (mc/create db "test")] + (is (= ["test"] (mg/collection-names db))) + (is (instance? MongoIterable (mg/collection-names db {:raw? true}))))) + +(deftest ^:integration test-collections + (let [db (new-db @client) + _ (mc/create db "test")] + (is (= ["test"] (map :name (mg/collections db)))) + (is (= ["test"] (map #(get % "name") (mg/collections db {:keywordize? false})))) + (is (instance? ListCollectionsIterable (mg/collections db {:raw? true}))))) + +(deftest ^:integration test-start-session + (is (instance? ClientSession (mg/start-session @client)))) \ No newline at end of file diff --git a/test/mongo_driver_3/collection_test.clj b/test/mongo_driver_3/collection_test.clj index 80dd2e9..2e3fd32 100644 --- a/test/mongo_driver_3/collection_test.clj +++ b/test/mongo_driver_3/collection_test.clj @@ -184,7 +184,7 @@ ;;; Integration -; docker run -it --rm -p 27017:27017 mongo:4.2 +; docker run -it --rm -p 27017:27017 mongo:4.2 --replset rs1 (def client (atom nil))