An idiomatic Clojure interface to Apache CouchDB.
- Ergonomic document API - Documents are treated like Clojure atoms.
swap!
,deref
, andreset!
operate on CouchDB documents.
- Minimizes bandwith
- Recently used documents are cached.
- Cached documents are monitored via the CouchDB changes endpoint to poll for changes efficiently.
- Ring middleware snapshots documents during requests.
slouch
is available from Clojars.
[com.balloneij/slouch "0.1.0"]
Everything you need is in the slouch.api
namespace.
(ns app.core
(:require [slouch.api :as slouch]))
Create a database. See API.
(with-open [db (slouch/database {:url "http://localhost:5984"
:name "albums"
:username "admin"
:password "hunter1"})]
;; your code
)
;; or
(slouch/with-database [db {:url "http://localhost:5984"
:name "albums"
:username "admin"
:password "hunter1"}]
;; your code
)
Documents are like Clojure atoms. See API.
(let [doc (slouch/insert db {:sales 0})]
(println @doc)
;; {:sales 0, :_id 5dde5901-6ee6-4084-a18e-b58f4487e4a2, :_rev 1-ea366df7bb92694d7de64184343c080e}
(swap! doc update :sales inc)
(println @doc)
;; {:sales 1, :_id 5dde5901-6ee6-4084-a18e-b58f4487e4a2, :_rev 2-68fb51089122a02a4d24f0910532b0f0}
(reset! doc {:sales 0})
(println @doc)
;; {:sales 0, :_id 5dde5901-6ee6-4084-a18e-b58f4487e4a2, :_rev 3-895e6de5e9418a64d7946247459bc769}
(slouch/remove db (slouch/id doc))
(println (slouch/exists? doc))
;; false
)
Query views. See API.
(slouch/rows db :albums/by-artist {:key "ABBA"})
;; => [{:key "ABBA" :value {:name "Arrival"}}
;; {:key "ABBA" :value {:name "Voyage"}}
;; {:key "ABBA" :value {:name "Waterloo"}}]
(-> (slouch/row db :albums/by-name "Thriller" {:include-docs? true})
(:doc)
(swap! update :sales inc)
(:sales))
;; => 70000001
Slouch only interfaces with one database on a CouchDB server at a time.
;; Database is closeable. It's meant to be used in with-open i.e. try with resources
;; to ensure the underlying processes stop and thread pool resources get released.
(slouch/database {:url "http://localhost:5984" :name "albums"
:username "webapp-user" :password "super-secret"})
;; Shorthand for (with-open [db (slouch/database config)])
(with-database [db {:url "http://localhost:5984" :name "albums"
:username "webapp-user" :password "super-secret"}])
{;; CouchDB url.
:url "http://localhost:5984"
;; The name of the database.
:name "albums"
;; Credentials.
:username "webapp-user"
:password "super-secret"
;; Whether to allow insecure https connections. Default is false.
:insecure? false
;; Size of thread pool for http connections to CouchDB. Default is 8.
:pool-threads 8
;; Seconds to keep connections open before automatically closing them.
;; Default is 60 seconds.
:pool-timeout 60
;; Milliseconds to wait before aborting a new connection attempt,
;; or 0, meaning no timeout (not recommended). Default is 5000 ms.
:connection-timeout 5000
;; Milliseconds of data silence to wait before abandoning an established connection,
;; or 0, meaning no timeout (not recommended). Default is 5000 ms.
:socket-timeout 5000
;; Seconds of session time remaining before reauthenticating.
;; Default is 60 seconds.
:session-auth-threshold 60
;; Seconds remaining before considering a session expired. At a minimum,
;; consider setting this value greater than socket-timeout + connection-timeout.
;; Default 30 seconds.
:session-timing-error 30
;; Milliseconds to keep a continuous connection open on /db/_changes to
;; watch for updates to documents stored in cache. A lower interval means
;; cache documents are added/removed to the watch more quickly to the watch list,
;; at the expense of reopening connections more frequently.
;; Default 10000 ms.
:feed-refresh-interval 10000
;; Minutes to keep documents stored in memory.
;; Default 15 min.
:cache-doc-ttl 15}
;; Insert new document with random uuid
(slouch/insert db {:name "21" :artist "Adele"})
;; Insert a new document with a specific id
(slouch/insert db "the-wall" {:name "The Wall" :artist "Pink Floyd"})
;; Get a document by id
(slouch/get db "abbey-road")
;; Get a document by id, or insert it if it does not exist
(slouch/get-or-insert db "spice" (fn [] {:name "Spice" :artist "Spice Girls"}))
;; Remove a document, no matter the revision
(slouch/remove db "the-wall")
;; Remove a document at specific revision
(slouch/remove db "the-wall" "3-2adcff8fb8b3f77825f627ad97464c80")
;; ID of a document
(slouch/id doc)
;; Revision of the current doc (or nil if it doesn't exist)
(slouch/rev doc)
;; Check if a document exists
(slouch/exists? doc)
;; Get a document from CouchDB
;; NOTE: Deref-ing will return the latest value unless called
;; within a snapshot context. See "Ring middleware" for more details
(deref doc)
@doc
;; Like swapping a Clojure atom, but writes to CouchDB
(swap! doc assoc :genre ["pop" "post-disco" "funk" "rock"])
(let [old-val @doc
new-val {:name "Thriller" :artist "Michael Jackson"}]
;; Set a new value iff the :_rev from an old value matches the rev
;; of the current document in CouchDB
(compare-and-set! doc old-val new-val))
;; Like reseting a Clojure atom, but writes to CouchDB
(reset! doc {:name "Thriller" :artist "Michael Jackson"})
(slouch/view db ddoc-view opts)
is the main interface for
querying CouchDB views. Additional functions are provided to make working
with the results more ergonomic.
To query a view, provide the design document and view name by one of two means:
- a vector
["design-doc" "view-name"]
. - a namespaced keyword
:design-doc/view-name
.
All view functions take view options.
;; Query a view for :offset, :rows, and :total-rows. See "View options"
(slouch/view db :albums/by-name)
(slouch/view db :albums/by-name {:skip 20})
;; Equivalent to (:rows (slouch/view db ddoc-view opts))
(slouch/rows db :albums/by-certification)
(slouch/rows db :albums/by-certification {:key "platinum"})
;; Equivalent to (first (:rows (slouch/view db ddoc-view (merge opts {:key k :limit 1}))))
(slouch/row db ["albums" "by-name"] "Millennium")
(slouch/row db ["albums" "by-name"] "Millennium" {:include-docs? true})
;; Equivalent to (->> (slouch/view db ddoc-view opts)
;; :rows
;; (map :doc))
(slouch/docs db :albums/by-name)
(slouch/docs db :albums/by-name {:start-key "1" :end-key "Thriller"})
;; Equivlanet to
;; (-> (view db ddoc-view (merge opts {:key k
;; :limit 1
;; :include-docs? true}))
;; :rows
;; first
;; :doc)
(slouch/doc db :albums/by-name "Thriller")
(slouch/doc db :albums/by-name "Thriller" {:stable? true})
View options come directly from the CouchDB view endpoint.
{;; Include conflicts information in response. Ignored if include-docs isn’t true. Default is false.
:conflicts? false
;; Return the documents in descending order by key. Default is false.
:descending? false
;; Stop returning records when the specified key is reached.
:end-key {:name "wish-you-were-here"}
;; Stop returning records when the specified document ID is reached. Ignored if end-key is not set.
:end-key-doc-id "255ce80b1928875f253f5fca670d0599"
;; Group the results using the reduce function to a group or single row. Implies reduce is true and the maximum group-level. Default is false.
:group? false
;; Specify the group level to be used. Implies group is true.
:group-level 2
;; Include the associated document with each row. Default is false.
:include-docs? false
;; Specifies whether the specified end key should be included in the result. Default is true.
:inclusive-end? true
;; Return only documents that match the specified key.
:key {:name "boston"}
;; Return only documents where the key matches one of the keys specified in the array.
:keys [{:name "millennium"} {:name "like-a-virgin"} {:name "purple-rain"}]
;; Limit the number of the returned documents to the specified number.
:limit 20
;; Use the reduction function. Default is true when a reduce function is defined.
:reduce? true
;; Skip this number of records before starting to return the results. Default is 0.
:skip 0
;; Sort returned rows. Setting this to false offers a performance boost. The total-rows and offset fields are not available when this is set to false. Default is true.
;; See Sorting Returned Rows https://docs.couchdb.org/en/stable/api/ddoc/views.html#sorting-returned-rows
:sorted? true
;; Whether or not the view results should be returned from a stable set of shards. Default is false.
:stable? false
;; Return records starting with the specified key.
:start-key {:name "baby-one-more-time"}
;; Return records starting with the specified document ID. Ignored if startkey is not set.
:start-key-doc-id "255ce80b1928875f253f5fca670d3e15"
;; Whether or not the view in question should be updated prior to responding to the user. Supported values: true, false, :lazy. Default is true.
:update true
;; Whether to include in the response an update-seq value indicating the sequence id of the database the view reflects. Default is false.
:update-seq? false}
{;; Include the Base64-encoded content of attachments in the documents that are included if include-docs is true. Ignored if include-docs isn’t true. Default is false.
:attachments? false
;; Include encoding information in attachment stubs if include-docs is true and the particular attachment is compressed. Ignored if include-docs isn’t true. Default is false.
:att-encoding-info? false
;; Deprecated by CouchDB. Use :stable and :update instead.
;; :ok is equivalent to {:stable true :update false}
;; :update_after is equivalent to {:stable true :update lazy}
;; The default behavior is equivalent to {:stable false :update true}.
:stale :ok}
wrap-db
handles each request inside a snapshot.
Inside a snapshot, the value of a document will stay the same throughout the duration of a request, unless an update occurs within the same snapshot.
Therefore, don’t be afraid to deref
a document multiple times within
a single request. At most, the document will be fetched from CouchDB one time.
(slouch/with-database [db config]
(-> handler
;; Add :db to incoming requests and execute handler inside a snapshot context
(slouch/wrap-db db)
;; or use a different key
(slouch/wrap-db :my-db db)
(run-webapp)))
- No means for solving document conflicts.
- Cannot handle document attachments.
- No means for seamless failover to other CouchDB instances.
- Cannot solve world hunger.
- Encode username and password so they aren’t stored in mem as plaintext
In case somewhere, somehow the db config gets
prn-str
‘ed (logs, stacktraces, etc.), it would be best if the username and password were at least base64 encoded.Maybe hide the values inside record and define a print-method to hide the password.
- Add a size limit to documents added to cache
- Reducible, transducer-ready view result
next-jdbc provides
next.jdbc/plan
which is a cool way to stream and process incoming SQL results. It could be fun to expirement with a similar system for Slouch and test to see if it has any merit speeding up view queries. - Lazily get ~rows~ It could be more efficiently to paginate rows results. For example, limit 100 records and then lazy-seq to get more.
- Multiple CouchDB instances
Support multiple CouchDB instances doing master-slave replication.
i.e.
- 1 master - write-only
- N replicas - read-only
A DBA could locate replicas at the same datacenters/device as the client, and then host the master in a central location.
- Support document attachments
0.1.0
Initial release
Copyright 2023 Isaac Ballone.
Distributed under the MIT License.