clouchdb

A Common Lisp library for interacting with CouchDb databases.

Overview

Clouchdb is a Common Lisp library for interacting with CouchDb databases. CouchDb is a document based database server. Clouchdb comes with a BSD Style License for maximum agreeableness.

Author: Peter Eddy (peter.eddy at the gmail.com)

Contents

Download and Installation

The current download link for clouchdb can be found at Clicki.net. Clouchdb may also be installed with ASDF.

Requirements:

ASDF Install

Something like the following should be all that's necessary to install and load clouchdb using ASDF-INSTALL:

(asdf-install:install 'clouchdb)  
(asdf:oos 'asdf:load-op '#:clouchdb)

ASDF-INSTALL will install library dependencies, though you must install a CouchDb server separately.

Unit tests

The clouchdb distribution comes with a unit test suite which uses the LIFT testing framework. To run the tests, follow the following steps:

(asdf:oos 'asdf:load-op '#:clouchdb-tests)
(in-package :clouchdb-tests)
(run-all-tests)

Note that if the CouchDb server is not running on the same host you will have to modify tests.lisp to point it to the appropriate host.

Examples

The distribution also includes an examples package:

(asdf:oos 'asdf:load-op '#:clouchdb-examples)
(in-package :clouchdb-examples)
(example1)
(example2)

Be sure to look at examples.lisp to understand what each example is doing.

Support and mailing lists

The following email lists have been provided by the common-lisp.net for clouchdb development and information:

Examples

The following clouchdb SLIME sessions demonstrate various aspects of the three major functional areas of the CouchDb API: Database API, Document API and View API.

NB: If you try these examples I suggest also viewing the results via CouchDb's HTML UI at http://localhost:8888/_utils/browse/index.html, of course you'll need to adjust the URL for the actual CouchDb server and port in use.

Example 1

The following example session demonstrates:
;; Create a package to try out clouchdb
CL-USER> (defpackage :clouchdb-user (:use :cl :clouchdb :parenscript))
#<Package "CLOUCHDB-USER">
CL-USER> (in-package :clouchdb-user)
#<Package "CLOUCHDB-USER">
;; Set our CouchDb host (default is "localhost"), and the database name for 
;; the examples
CLOUCHDB-USER> (set-connection :host "odeon" :db-name "test-db")
; No value
;; Get CouchDb Server Information by specifying a nil DB name
CLOUCHDB-USER> (get-db-info :db-name nil)
((:COUCHDB . "Welcome") (:VERSION . "0.7.0a575"))
;; Create the database
CLOUCHDB-USER> (create-db)
((:OK . T))
;; Create a document with one field, and give it an ID of "gjc"
CLOUCHDB-USER> (create-document '((:name . "Gaius Julius Caesar")) :id "gjc")
((:OK . T) (:ID . "gjc") (:REV . "1479031852"))
;; Fetch the document we just created 
CLOUCHDB-USER> (get-document "gjc")
((:_ID . "gjc") (:_REV . "1479031852") (:NAME . "Gaius Julius Caesar"))
;; Add a field to "gjc"
CLOUCHDB-USER> (put-document (cons '(:lover . "Servilia Caepionis") *))
((:OK . T) (:ID . "gjc") (:REV . "1460552879"))
;; Get the updated document
CLOUCHDB-USER> (get-document "gjc")
((:_ID . "gjc") (:_REV . "1460552879") (:LOVER . "Servilia Caepionis") 
 (:NAME . "Gaius Julius Caesar"))

Example 2

Demonstrating:
;; Create, or drop and recreate, the current database
CLOUCHDB-USER> (create-db :if-exists :recreate)
((:OK . T))
;; Create a document that will have it's ID assigned by the CouchDb server
CLOUCHDB-USER> (create-document '((:size . "medium") (:color . "blue")))
((:OK . T) (:ID . "C731D3A3698DA144FB35EDA9737917F2") (:REV . "3363852140"))
;; CouchDb generated IDs are too large to use easily in an
;; interactive example like this, so create another document
;; with a shorter ID to demonstrate property value updates
CLOUCHDB-USER> (create-document '((:size . "large") (:color . "blue")) 
                                :id "someid")
((:OK . T) (:ID . "someid") (:REV . "3181950830"))
;; Change :color property
CLOUCHDB-USER> (let ((doc (get-document "someid")))
                 (setf (document-property :color doc) "green")
                 (put-document doc))
((:OK . T) (:ID . "someid") (:REV . "4275808446"))
;; Show that the new property stuck
CLOUCHDB-USER> (get-document "someid")
((:_ID . "someid") (:_REV . "4275808446") (:SIZE . "large") (:COLOR . "green"))
;; Get revision information for this document (formatted for legibility)
CLOUCHDB-USER> (get-document "someid" :revision-info t)
((:_ID . "someid") (:_REV . "4275808446") (:SIZE . "large") (:COLOR . "green") 
 (:_REVS_INFO 
  ((:REV . "4275808446") (:STATUS . "disk")) 
  ((:REV . "3181950830") (:STATUS . "disk"))))
;; Since the first revision is still available (:status . "disk") we 
;; can still retrieve it
CLOUCHDB-USER> (get-document "someid" :revision "3181950830")
((:_ID . "someid") (:_REV . "3181950830") (:SIZE . "large") (:COLOR . "blue"))

;; In the following document, the :tags field has an array value,
;; the :demographics field has a map value, and the :religion map 
;; key also has an associated map value.
CLOUCHDB-USER> (create-document '((:name . "Czech Republic")
                                  (:tags . ("country" "European"))
                                  ;; Field using map property value:
                                  (:demographics . ((:population . 10230000)
                                                     ;; A nested map property:
                                                     (:religion . ((:agnostic . 0.59)
                                                                   (:roman-catholic . 0.26)
                                                                   (:protestant . 2.5)))
                                                     (:political-system . "democracy"))))
                                :id "czechrepublic")
((:OK . T) (:ID . "czechrepublic") (:REV . "4272625130"))
;; Let's see what this document looks like (formatted for legibility)
CLOUCHDB-USER> (get-document "czechrepublic")
((:_ID . "czechrepublic") (:_REV . "3929202819") 
 (:NAME . "Czech Republic") (:TAGS "country" "european") 
 (:DEMOGRAPHICS (:POPULATION . 10230000) 
                (:RELIGION (:AGNOSTIC . 0.59) (:ROMAN-CATHOLIC . 0.26) (:PROTESTANT . 2.5)) 
                (:POLITICAL-SYSTEM . "democracy")))
;; Get all documents, results again formatted for legibility
CLOUCHDB-USER> (get-all-documents)
((:TOTAL_ROWS . 3) (:OFFSET . 0) 
 (:ROWS 
  ((:ID . "C731D3A3698DA144FB35EDA9737917F2") (:KEY . "C731D3A3698DA144FB35EDA9737917F2") 
   (:VALUE (:REV . "3363852140")))
  ((:ID . "czechrepublic") (:KEY . "czechrepublic") (:VALUE (:REV . "4272625130"))) 
  ((:ID . "someid") (:KEY . "someid") (:VALUE (:REV . "4275808446")))))

Example 3

Demonstrating:
;; Create current database if it doesn't already exist.
;; An (:IGNORED . T) result indicates that the create
;; was ignored because the database already existed.
CLOUCHDB-USER> (create-db :if-exists :ignore)
((:OK . T) (:IGNORED . T))

;; Create some documents representing various cities and their 
;; associated countries.
CLOUCHDB-USER> (create-document '((:city . "New York City")
                                  (:country . "US"))
                                :id "nyc")
((:OK . T) (:ID . "nyc") (:REV . "1023292373"))
CLOUCHDB-USER> (create-document '((:city . "Amsterdam")
                                  (:country . "NL"))
                                :id "amst") 
((:OK . T) (:ID . "amst") (:REV . "3679905075"))
CLOUCHDB-USER> (create-document '((:city . "Chicago")
                                  (:country . "US"))
                                :id "chi") 
((:OK . T) (:ID . "chi") (:REV . "1627558845"))

;; Create a persistent view document to find cities in the
;; Netherlands and also to find cities by country key. 
;; Note: Expressions within the (ps) expressions are Parenscript,
;; a lispy way to generate JavaScript.
CLOUCHDB-USER> (create-view "cities"
                            (cons "country"
                                  (ps (lambda (doc)
                                        (with-slots (country) doc
                                          (map country doc)))))
                            (cons "nl"
                                  (ps (lambda (doc)
                                        (with-slots (country) doc
                                          (if (eql "NL" country)
                                              (map country doc)))))))
((:OK . T) (:ID . "_design/cities") (:REV . "3690565831"))

;; Invoke "nl" view to find cities in the Netherlands
CLOUCHDB-USER> (invoke-view "cities" "nl")
((:TOTAL_ROWS . 1) (:OFFSET . 0) 
 (:ROWS 
  ((:ID . "amst") (:KEY . "NL") (:VALUE (:_ID . "amst") 
   (:_REV . "3679905075") (:CITY . "Amsterdam") (:COUNTRY . "NL")))))

;; Invoke "country" view created above and search for US cities
CLOUCHDB-USER> (invoke-view "cities" "country" :key "US")
((:TOTAL_ROWS . 3) (:OFFSET . 1) 
 (:ROWS 
  ((:ID . "chi") (:KEY . "US") (:VALUE (:_ID . "chi") 
   (:_REV . "1627558845") (:CITY . "Chicago") (:COUNTRY . "US"))) 
  ((:ID . "nyc") (:KEY . "US") (:VALUE (:_ID . "nyc") 
   (:_REV . "1023292373") (:CITY . "New York City") (:COUNTRY . "US")))))

;; Note: the two responses above have been formatted for legibility. Also,
;; see CouchDb documentation for why 3 is returned for TOTAL_ROWS when
;; there are only two results.

API Reference

Server Connection and Database Management API The functions described in this section have to do with setting CouchDb connection values, creating, deleting and getting information about databases and information about the CouchDb server.

[Function]
create-db &key db-name if-exists

Create a database. The db-name can be specified, otherwise attempts to create the database named in the current context (either through (set-connection) or (with-connection).

The if-exists parameter defaults to :fail, which will raise an error if the database already exists. A value of :ignore will simply ignore the this error. A value of :recreate will delete the database if it exists, and then recreate it.

[Function]
delete-db &key db-name if-missing

Delete a database. The db-name can be specified, otherwise attempts to delete the database named in the current context (either through (set-connection) or (with-connection).

If :ignore is specified for the if-missing parameter, errors resulting from the attempt to delete a non-existent database are ignored.

[Function]
get-db-info &key db-name

Returns database information for the connection and database in the current context, or, if the db-name key parameter is specified, for that database.

Example:

(get-db-info)
=> ((:DB_NAME . "test-db") (:DOC_COUNT . 3) (:UPDATE_SEQ . 4))

[Function]
list-dbs

Returns a list of database names available in the current connection.

[Function]
set-connection &key host db-name protocol port => no value

Sets the host name, database name, protocol ("http" or "https") and port number for the top-level connection to the CouchDb server. Default connection settings are host="localhost", protocol="http", port="8888" and database="default".

See (with-connection)

[Macro]
with-connection (&key host db-name protocol port) &body body => value returned by body

Executes the contained statements in the context of any of the specified connection values. Sets the host name, database name, protocol ("http" or "https") or port number of the CouchDb server to use in the expressions in the body.

Example:

;; Get document from specified host and database
(with-connection (:host "cornichon.cucumber.net" :db-name "rfc")
  (get-document "2616"))

See (set-connection)

Document API

Documents in clouchdb are identified by a document ID, their content takes the form of an associative list. The native document representation in CouchDb is a JSON object. Using cl-json, clouchdb automatically translates documents between JSON and Lisp associative lists.

Special Properties

When a document is created CouchDb assigns special properties to that document, these properties cannot be modified by clients. The special properties include the document's ID (:_id) and the document revision number (:_rev). All special properties begin with an underscore (_) symbol.

(create-document '((:name . "Maxwell Smart") (:agent . 86)) :id "max")
=> ((:OK . T) (:ID . "max") (:REV . "3789799231"))

(get-document "max")
=> ((:_ID . "max") (:_REV . "3789799231") (:NAME . "Maxwell Smart") (:AGENT . 86))

Please refer to the CouchDb Document API for general CouchDb document information.

[Function]
create-document doc &key id

Create a new document, optionally specifying the document's ID. This method simply calls (post-document) if an ID is specified, otherwise (put-document).

Example:

(create-document '((:string . "string") 
                   (:number . 42)
                   (:array . ("one" 2 "nine"))
                   (:map . ((:foo . "bar")
                            (:size . 3)
                            (:colors . ("red" "blue" "green")))))
                 :id "example")

See (put-document) (post-document)

[Function]
delete-document &key document id revision

Delete the specified document, which must include the standard CouchDb special variables :_ID and :_REV. The document may also identified by ID. If revision is specified, delete the specific revision of identified document, otherwise, delete the most current revision by fetching the document by ID and using its :_rev value for the deletion.

[Function]
document-property name doc

Get the value of the named document property or nil if property does not exist. This function can be used with setf to set property values as well:

(create-document '((:name . "Maxwell Smart") (:agent . 86)) :id "max")

(document-property :name (get-document "max"))
=> "Maxwell Smart"

(let ((max (get-document "max")))
  (setf (document-property :name max) "Maxwell Smart, Secret Agent")
  (put-document max))

(get-document "max")
=> "Maxwell Smart, Secret Agent"

[Function]
get-all-documents &key descending

Return ID and current revision information for all documents, ordered by ascending document ID. If descending is non-nil, returns documents in reverse order.

[Function]
get-document id &key revision revisions revision-info

Get document by ID. If revision is specified attempts to retrieve specified revision of identified document. If revisions is non-nil, returns brief revision information for identified document. If revision-info is non-nil, returns more detailed revision information for document. The revision, revisions, and revision-info parameters are mutually exclusive.

This function returns nil if no document matching ID is found.

[Function]
post-document doc

Create a document and let the server assign an ID.

See (create-document) (put-document)

[Function]
put-document doc &key id

Create a new document or update an existing one. If the document is new, the ID must be specified. If the document has been fetched from the server (and still retains its :_* CouchDb special properties) then no ID need be specified. If an ID is specified and it differs from the existing :_id value, then a new document is created with the specified ID and any non-special properties of the document.

Example:

;; Create document "A"
(put-document '((:name . "Laraby")) :id "A")

;; Copy to new document "B"
(put-document (get-document "A") :id "B")

;; Add field to document "B"
(put-document (cons '(:new-field . "new-value")) (get-document "B"))

See (create-document) (post-document)


Views API

Views are the mechanism with which documents are queried in CouchDb. There are two types of views in CouchDb: ad hoc and persistent. As you might expect, persistent views are stored in the database. Ad hoc views are not, they are sent from the client each time they're used. Native CouchDb views are expressed in JavaScript though it is not necessary to use JavaScript directly thanks to Parenscript.

Please refer to CouchDb View API Documentation for general information about CouchDb views.

[Function]
ad-hoc-view view

Executes a one-time, non persistent view (query). The view is specified as a JavaScript anonymous function.

Example:

(create-document '((:name . "Laraby")))
(ad-hoc-view "function(doc) { if (doc.name == 'Laraby') { map(null,doc.name) } }")

Ok, but this is Lisp and JavaScript looks exotic and scary. To solve this problem we use Parenscript. The following expression generates the same view with a more familiar syntax:

(create-document '((:name . "Laraby")))
(ad-hoc-view (ps (lambda (doc)
                   (with-slots (name) doc
                     (if (eql name "Laraby")
                       (map nil doc))))))

Note that it is not necessary for every document in the database to have a field called 'name'. This view will return only those documents that have a name field and where the value of that name field is "Laraby". No errors result from documents with no name field.

The (with-slots) expression above can be eliminated, if desired, like so:

(ad-hoc-view (ps (lambda (doc)
                   (if (eql doc.name "Laraby")
                     (map nil doc))))))

See (create-view)

[Function]
create-view {view-definition}*

Creates a view document containing one or more view definitions

Example:

;; Views defined with Parenscript
(create-view "names"
  (cons "laraby"
        (ps (lambda (doc)
              (with-slots (name) doc
                (if (eql "Laraby" name)
                  (map name doc))))))
  (cons "name"
        (ps (lambda (doc)
              (with-slots (name) doc
                (map name doc))))))

See (invoke-view)

[Function]
delete-view id &key rev

Delete view document identified by id. If revision is specified, delete specific revision of view document.

See (create-view)

[Function]
invoke-view id view :key key

Invoke specified view in identified view document.

Example:

;; Document to query
(create-document '((:name . "Laraby")))

;; Views defined with Parenscript
(create-view "names"
  (cons "laraby"
        (ps (lambda (doc)  ;; parameter-less view
              (with-slots (name) doc
                (if (eql "Laraby" name)
                  (map nil doc))))))
  (cons "name"
        (ps (lambda (doc)  ;; parameter view
              (with-slots (name) doc
                (map name doc))))))

;; Find document by invoking parameter-less "laraby" view
(invoke-view "names" "laraby")

;; Find document by invoking "name" view with key parameter
(invoke-view "names" "name" :key "Laraby")

See (create-view) (ad-hoc-view)

Symbol Index

ad-hoc-view
create-db
create-document
create-view
delete-db
delete-document
delete-view
document-property
get-all-documents
get-db-info
get-document
invoke-view
list-dbs
post-document
put-document
set-connection
with-connection

Back to Common-lisp.net.

Valid XHTML 1.0 Strict