Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Working on a CouchDB type for Clojure
;; Clutch provides a pretty comprehensive API, but I'm frustated that 95% of database
;; interactions require using something other than the typical Clojure vocabulary of
;; assoc/conj/dissoc/get/seq/reduce/etc, even though those semantics are entirely appropriate
;; (modulo the whole stateful database thing).
;;
;; This is (the start of) an attempt to create a type to provide most of the
;; functionality of Clutch with a more pleasant, concise API (it uses the Clutch API
;; under the covers, and rare operations would generally remain accessible only
;; at that lower level).
;;
;; Would like to eventually add:
;;
;; * support for views (aside from _all_docs via seq)
;; * support for _changes (via a seque?), maybe a more natural place than the
;; (free-for-all) pool of watches in Clutch's current API
;; * support for bulk update, maybe via IReduce?
;; * Other CouchDB types:
;; ** to provide specialized query interfaces e.g. cloudant indexes
;; ** to return custom map and vector types to support e.g.
;;
;; (assoc-in! db ["ID" :key :key array-index] x)
;; (update-in! db ["ID" :key :key array-index] assoc :key y)
;;
;; Feedback wanted!
;;
;; Comment below
;; or at
;; http://groups.google.com/group/clojure-clutch/browse_frm/thread/4520716eb37a90cc
;; or
;; @cemerick
;; or
;; cemerick in #clojure
;;
;; *requires [com.ashafa/clutch "0.3.0-SNAPSHOT"] (latest at https://github.com/cemerick/clutch)*
(ns user
(:require [com.ashafa.clutch :as db])
(:refer-clojure :exclude (conj! assoc! dissoc!)))
(defprotocol CouchOps
"Defines side-effecting operations on a CouchDB database.
All operations return the CouchDB database reference —
with the return value from the underlying clutch function
added to its :result metadata — for easy threading and
reduction usage."
(create! [this] "Ensures that the database exists, and returns the database's meta info.")
(conj! [this document]
"PUTs a document into CouchDB. Accepts either a document map (using an :_id value
if present as the document id), or a vector/map entry consisting of a
[id document-map] pair.")
(assoc! [this id document]
"PUTs a document into CouchDB. Equivalent to (conj! couch [id document]).")
(dissoc! [this id-or-doc]
"DELETEs a document from CouchDB. Uses a given document map's :_id and :_rev
if provided; alternatively, if passed a string, will blindly attempt to
delete the current revision of the corresponding document."))
(defn- with-result-meta
[couch result]
(vary-meta couch assoc :result result))
(deftype CouchDB [url meta]
CouchOps
(create! [this] (with-result-meta this (db/get-database url)))
(conj! [this doc]
(let [[id doc] (cond
(map? doc) [(:_id doc) doc]
(or (vector? doc) (instance? java.util.Map$Entry)) doc)]
(with-result-meta this (db/put-document url doc :id id))))
(assoc! [this id document] (conj! this [id document]))
(dissoc! [this id]
(if-let [d (if (#'db/document? id)
id
(get this id))]
(with-result-meta this (db/delete-document url d))
(with-result-meta this nil)))
clojure.lang.ILookup
(valAt [this k] (db/get-document url k))
(valAt [this k default] (or (.valAt this k) default))
clojure.lang.Counted
(count [this] (:doc_count (db/database-info url)))
clojure.lang.Seqable
(seq [this]
(->> (db/all-documents url {:include_docs true})
(map :doc)
(map #(clojure.lang.MapEntry. (:_id %) %))))
clojure.lang.IMeta
(meta [this] meta)
clojure.lang.IObj
(withMeta [this meta] (CouchDB. url meta)))
(defn couch
([url] (CouchDB. url nil))
([url meta] (CouchDB. url meta)))
;;;; REPL demo
=> (def db (couch "test"))
#'user/db
=> (create! db)
#<CouchDB user.CouchDB@3f460a4a>
=> (:result (meta *1))
#com.ashafa.clutch.utils.URL{:protocol "http", :username nil, :password nil,
:host "localhost", :port -1, :path "test", :query nil, :disk_format_version 5,
:db_name "test", :doc_del_count 0, :committed_update_seq 0, :disk_size 79,
:update_seq 0, :purge_seq 0, :compact_running false, :instance_start_time
"1324037686108297", :doc_count 0}
=> (reduce conj! db (for [x (range 5000)]
{:_id (str x) :a [1 2 x]}))
#<CouchDB user.CouchDB@71d1be4e>
=> (count db)
5000
=> (get-in db ["68" :a 2])
68
=> (def copy (into {} db))
#'user/copy
=> (get-in copy ["68" :a 2])
68
=> (first db)
["0" {:_id "0", :_rev "1-79fe783154bff972172bc30732783a68", :a [1 2 0]}]
=> (dissoc! db "68")
#<CouchDB user.CouchDB@48f50903>
=> (get db "68")
nil
=> (assoc! db :foo {:a 6 :b 7})
#<CouchDB user.CouchDB@79d7999e>
=> (:result (meta *1))
{:_rev "1-ac3fe57a7604cfd6dcca06b25204b590", :_id ":foo", :a 6, :b 7}
@daveray

This comment has been minimized.

Copy link

commented Dec 19, 2011

Neat! This is probably a dumb idea, but I always thought it would be interesting to implement swap! for Couch documents by retrying as long as Couch responds with 409/conflict.

@cemerick

This comment has been minimized.

Copy link
Owner Author

commented Dec 19, 2011

Yup, that's exactly where I'm heading. :-)

@daveray

This comment has been minimized.

Copy link

commented Dec 19, 2011

Cool! And if two people independently have the same idea, it can't possibly be bad :)

@cemerick

This comment has been minimized.

Copy link
Owner Author

commented Jan 25, 2012

This is now in clutch master, available as [com.ashafa/clutch "0.3.1-SNAPSHOT"]. Feedback most welcome on the clutch ML:

http://groups.google.com/group/clojure-clutch

@benatkin

This comment has been minimized.

Copy link

commented Apr 27, 2012

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.