Create a gist now

Instantly share code, notes, and snippets.

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
daveray 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
Owner

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

@daveray
daveray commented Dec 19, 2011

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

@cemerick
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment