clouchdb

A Common Lisp library for interacting with CouchDb databases.

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

Contents

Overview

Clouchdb is a Common Lisp library for interacting with CouchDb databases. CouchDb is a document based, schema-less database server.

Design Goals

The library was written with the goals that it be:

License

Clouchdb, comes with a BSD Style License for maximum agreeableness.

News

Download and Installation

The current download link for clouchdb can be found at Clicki.net. Clouchdb may also be installed 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)
;; If CouchDb is running on a different host
(set-connection :host "db-host")
(run-all-tests)

Examples

The distribution also includes an examples package:

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

The file examples.lisp contains comments describing each example in detail.

CVS Access

Clouchdb project hosting is graciously provided by common-lisp.net, the CVS repository may be checked out anonymously as follows:

cvs -z3 -d :pserver:anonymous:anonymous@common-lisp.net:/project/clouchdb/cvsroot co clouchdb

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 bulit-in HTML UI at http://localhost:5894/_utils/browse/index.html, adjust the URL host and port 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))
#<Package "CLOUCHDB-USER">

CL-USER> (in-package :clouchdb-user)
#<Package "CLOUCHDB-USER">

;; If CouchDb is running on a different host set that host
;; name (default is "localhost"), also set the database name
;; to be used in this session (default database name is "default")
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.8.0-incubating"))

;; Create database "test-db", which we named above
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> (put-document 
                 (set-document-property (get-document "someid")
                                        :color "green")
((:|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
CLOUCHDB-USER> (get-document "someid" :revision-info t)
((:|_id| . "someid") (:|_rev| . "4275808446") (:SIZE . "large") (:COLOR . "green") 
 (:|_revs_info|
  ((:|rev| . "4275808446") (:|status| . "available")) 
  ((:|rev| . "3181950830") (:|status| . "available"))))

;; Since the first revision is still available 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
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 in current database
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")))))

;; Get just the ID values for all documents in the current database
(query-document '(:|rows| :|id|) (get-all-documents))
("someid" "czechrepublic" "C731D3A3698DA144FB35EDA9737917F2")

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 . "Rotterdam")
                                  (:country . "NL"))
                                :id "rott") 
((:|ok| . T) (:|id| . "rott") (:|rev| . "46041399"))
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, cities by country key, and cities by country 
;; and city. 

;; Note: Expressions within the (ps-view) macro are Parenscript,
;; a lispy way to generate JavaScript. Unquoted keyword symbols in Lisp 
;; field names should be referenced in Parenscript with the double
;; asterix names, as in the examples below. This is because JavaScript
;; is case sensitive and unquoted Lisp symbols are converted to upper
;; case. The double asterix syntax causes Parenscript to generate 
;; upper case JavaScript property names.

CLOUCHDB-USER> (create-ps-view "cities"
                 ;; Index of cities in the Netherlands
                 (ps-view ("nl")
                   (defun map (doc)
                     (with-slots (*country* *city*) doc
                       (if (eql "NL" *country*)
                         (emit *city* doc)))))
                 ;; Index by country
                 (ps-view ("country")
                   (defun map (doc)
                     (with-slots (*country*) doc
                       (emit *country* doc)))))
                 ;; Index by country and city
                 (ps-view ("multi")
                   (defun map (doc)
                     (with-slots (*country* *city*) doc
                       (emit (list *country* *city*) 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| . 2) (:|offset| . 0) 
 (:|rows| 
  ((:|id| . "amst") (:|key| . "Amsterdam") (:|value| (:|_id| . "amst") 
   (:|_rev| . "3679905075") (:CITY . "Amsterdam") (:COUNTRY . "NL")))
  ((:|id| . "rott") (:|key| . "Rotterdam") (:|value| (:|_id| . "rott") 
   (:|_rev| . "46041399") (:CITY . "Rotterdam") (:COUNTRY . "NL")))))

;; Use "nl" view to find a specific city in the Netherlands
CLOUCHDB-USER> (invoke-view "cities" "nl" :key "Rotterdam")
((:|total_rows| . 2) (:|offset| . 0) 
 (:|rows| 
  ((:|id| . "rott") (:|key| . "Rotterdam") (:|value| (:|_id| . "rott") 
   (:|_rev| . "46041399") (:CITY . "Rotterdam") (:COUNTRY . "NL")))))

;; Invoke "country" view to 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")))))

;; Find the US city Chicago using the "multi" index:
CLOUCHDB-USER> (invoke-view "cities" "multi" :key '("US" "Chicago"))
((:|total_rows| . 3) (:|offset| . 1) 
 (:|rows| 
  ((:|id| . "chi") (:|key| . "US") (:|value| (:|_id| . "chi") 
   (:|_rev| . "1627558845") (:CITY . "Chicago") (:COUNTRY . "US")))))

;; Use "multi" index to find all cities with country codes that start with "N"
CLOUCHDB-USER> (invoke-view "cities" "multi" :start-key '("N") :end-key '("O"))
((:|total_rows| . 3) (:|offset| . 0) 
 (:|rows| 
  ((:|id| . "amst") (:|key| . "Amsterdam") (:|value| (:|_id| . "amst") 
   (:|_rev| . "3679905075") (:CITY . "Amsterdam") (:COUNTRY . "NL")))
  ((:|id| . "rott") (:|key| . "Rotterdam") (:|value| (:|_id| . "rott") 
   (:|_rev| . "46041399") (:CITY . "Rotterdam") (:COUNTRY . "NL"))))))

API Reference

Server Connection and Database Management API

The API described in this has to do with managing CouchDb server information and the creation and deletion of databases.

[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.

Example:

;; Create the database named in the current connection settings
(set-connection :db-name "tvland")
(create-db)
=> ((:|ok| . T))

;; Specify name of database to create, if it already exists
;; then ignore the request (don't generate error, don't
;; recreate database), return (:ignored . T) if database did
;; exist
(create-db :db-name "tvland" :if-exists :ignore)
=> ((:|ok| . T) (:IGNORED . T))

;; Create named db, if it already exists, drop it and 
;; recreate it
(create-db :db-name "tvland" :if-exists :recreate)
=> ((:|ok| . T))

[Function]
compact-db &key db-name

Force a manual compaction of a database. If db-name is specified, compacts that database, otherwise compacts the current database.

[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.

Example:

;; Create the database named in the current connection settings
(set-connection :db-name "tvland")
(delete-db)
=> ((:|ok| . T))

;; Specify name of database to delete, if it doesn't exist
;; then ignore the request (don't generate error), return 
;; ((:ERROR . "not_found") (:REASON . "missing")) if 
;; database did not exist
(delete-db :db-name "tvland" :if-missing :ignore)
=> ((:ERROR . "not_found") (:REASON . "missing"))

[Special Variable]
*document-fetch-fn*

A function which is called whenever a document is fetched from the database. The function takes one argument, the document, and returns the potentially modified document.

See *document-update-fn*

[Special Variable]
*document-update-fn*

A function which is called whenver a document is sent to the database via put-document, create-document or post-document functions. The function should take one parameter, which will be the document, and it should return one value, which should be the potentially modified document.

;; Create a function that adds or updates a timestamp field in a
;; document and returns the modified document
(defun time-stamper (doc) 
  (set-document-property doc :timestamp (get-universal-time)))

;; Cause time-stamper to be invoked with each document as
;; it's updated or created
(setf *document-fetch-fn* #'time-stamper)

;; Test the new time stamping feature
(create-document '((:hello . "there")) :id "test")
=> ((:|ok| . T) (:|id| . "test") (:|rev| . "3721228336"))

;; The new document should have been created with a timestamp, 
;; let's see if it worked:
(get-document "test")
=> ((:|_id| . "test") (:|_rev| . "3721228336") (:TIMESTAMP . 3422649324) 
  (:HELLO . "there"))

See *document-fetch-fn*

[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 the databases that exist on the current database host.

(list-dbs)
=> ("default" "example1" "example2" "example3")

[Function]
set-connection &key host db-name protocol port document-update-fn document-fetch-fn => 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="5984" and database="default".

Functions may be specified that will be invoked automatically each time a document is created or updated (document-update-fn) and each time a document is fetched (document-fetch-fn). These functions must take one argument, the document, and return the potentially modified document.

See (with-connection)

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

Executes the statements in body in the context 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.

Additionally functions that are called as documents are created or modified (document-update-fn) and when documents are fetched (document-fetch-fn) may be specified. These functions take one parameter, the document being sent to or fetched from the database, and return a potentially modified document.

Example:

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

;; Copy document identified by "someid" from database "otherdb" to 
;; current database, use "copy-of-someid" for copied document ID.
(put-document
  (with-connection (:db-name "otherdb")
    (get-document "someid"))
  :id "copy-of-someid")

See (set-connection)


Document API

The Document ID

Documents in clouchdb are identified by a document ID string which must be unique within the database that will contain the document. The ID string may either be specified when the document is created or it can be provided by the CouchDb server.

Document Content

Document content takes the form of an associative list. The car of each associative list element represents the document field name, the cdr contains the value. Field names are specified as keyword symbols. The following example demonstrates the creation of a simple document with two fields, :name and :agent, and with a specified document ID:

(create-document '((:name . "Max") (:agent . 86)) :id "agent86")
(get-document "agent86")
=> ((:|_id| . "agent86") (:|_rev| . "3674093994") (:NAME . "Max") (:AGENT . 86))
  

By giving keyword symbols their special significance as field names identifiers, clouchdb is able to distinguish between field names and field values in certain situations which would otherwise be ambiguous. For example, keyword symbols allow clocuhdb to distinguish between associative lists and lists that contain other, non-associative lists.

Field names in CouchDb are case sensitive. Field names specified with unquoted keyword symbols are normally converted to upper case by Lisp and this results in upper case field names in the CouchDb server. Use quotes in symbol names to specify mixed or lower case field names, like so:

;; Create a document with a mixed case field name. This document
;; will appear in the database as, "MixedCaseName"
(create-document '((:|MixedCaseName| . "Value")) :id "mixed-case")
(get-document "mixed-case")
=> ((:|_id| . "mixed-case") (:|_rev| . "2016717365") (:|MixedCaseName| . "Value"))
  

The native document representation in the CouchDb protocol is a JSON object. Clouchdb translates documents to and from JSON as necessary.

The value of a document field may be a string, a number, a list, a boolean, or an associative list. Document field values may be nested to create arbitrarily complex document structures.

Representation of Field Names and Values
Description Lisp JSON
Boolean (:boolean . t)
(:boolean . nil)
"BOOLEAN" : true
"BOOLEAN" : false | null
String (:string . "Example") "STRING" : "Example"
Keywords (:keyword . example)
(:keyword . :example)
(:keyword . |Example|)

Note: Keywords are always converted to strings
"KEYWORD" : "EXAMPLE"
"KEYWORD" : "EXAMPLE"
"KEYWORD" : "Example"
Number (:number . 42) "NUMBER" : 42
Floating Point Number (:number . 42.0) "NUMBER" : 42.0
Lisp List/JSON Array (:list . (1 2 3))
(:list 1 2 3)
"LIST" : [1,2,3]
Lisp Associative Array/JSON Object (:person . ((:name . "Bruce") (:gender . m))) "PERSON" : {"NAME": "Bruce", "GENDER": "M"}

Though these types are simple, lists and associative lists may contain other lists or associative lists, permitting a rich document structure.

(create-document '((:string . "String Value")
                   (:number . 42.0)
                   (:list . (milk eggs "green beans"))
                   (:alist . ((:string . "Another String")
                              (:size . 3)
                              (:false . t)
                              (:list . ("un" "deux" "trois"))
                              (:another-alist . ((a . "A") (b . "B")))))))

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. CouchDb uses lower case for these special properties therefore, as of Clouchdb version 0.0.8, they will always appear as quoted keyword symbols.

(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]
as-keyword-symbol string

Create a keyword symbol from a string, encode case information in the result. This function translates Json field names from CouchDb into the Lisp keyword symbols used in documents.

Example:

  ;; Lower case
  (as-keyword-symbol "lowercase")
  => :|lowercase|

  ;; Upper case
  (as-keyword-symbol "UPPER-CASE")
  => :UPPER-CASE

  ;; Mixed case
  (as-keyword-symbol "MixedCase")
  =>:|MixedCase|

See (as-field-name-string)

[Function]
as-field-name-string symbol

Convert a field name keyword symbol to a camelCase style string. This function produces the field name in the format that will be used and visible in CouchDb.

Example:

  ;; Upper case
  (as-field-name-string :case)
  => "CASE"

  ;; Lower case
  (as-field-name-string :|case|)
  => "case"

  ;; Mixed case
  (as-field-name-string :|MixedCase|)
  => "MixedCase"

See (as-keyword-symbol)

[Function]
create-document doc &key id

Create a new document, optionally specifying the document's ID. This method simply calls (put-document) if an ID is specified, otherwise it calls (post-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 if-missing

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, deletes the specific revision of identified document. If no revision is specified, deletes the most current revision by fetching the document by ID and using its :|_rev| value for the deletion.

Signals a document-missing error if document does not exist, unless the :if-missing keyword parameter is set to :ignore

[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"

;; document-property is setf-able, but note that the result is
;; not persisted automatically (use put-document for that)

(setf (document-property :name (get-document "max")) "Max")
=>((:|_id| . "max") (:|_rev| . "1213746364") (:NAME . "Max") (:AGENT . 86))

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

(put-document (setf (document-property :name (get-document "max")) "Max"))
=>((:|ok| . T) (:|id| . "max") (:|rev| . "1262996208"))

(get-document "max")
=>((:|_id| . "max") (:|_rev| . "1262996208") (:NAME . "Max") (:AGENT . 86))

See (set-document-property)

[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.

Note: Revision information is used by CouchDb to support database replication. You should not use revision data to build version control type systems becuase older revisions will be deleted automatically when no longer needed by the database.

[Function]
post-document doc

Create a document and let the server assign an ID. An existing :|_id| field in the document will be ignored, the server will create a new document and assign it a new ID. This therefore is an easy way to copy documents. The return value includes the server-assigned document ID in the :|id| property.

Example:

;; Create a document, let server assign an ID
(post-document '((:field . "value")))

=> ((:|ok| . T) (:|id| . "4A0FF20F6AE5168B771BC41D4557F650") (:|rev| . "16873930"))

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 an 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 parameter ID is provided and it differs from the :|_id| value in the document, then a new document is created with the provided ID and any non-special properties of the document.

Example:

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

;; Copy document A 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)

[Function]
query-document query document

This function can be used to extract data from complex documents or other CouchDb data such as results returned from views. The query parameter is a list containing zero or more query expressions. A query expression can be either a keyword symbol, which simply matches the keyword field name in the document, wild card symbols, or a function.

Note that this function does not represent any CouchDb API, nor does it communicate with the database other than when functions are used in the query and these functions communicate with the database.

Query Operators
Type Description
keyword symbol Matches field name in document
:* Wild card, matches any symbol at the current level in the document
:** Recursive wild card, matches subsequent symbol at any level in the document below the current level
function The function should accept a single parameter. This parameter will contain the value matched by the previous query term, or the initial document if the function is the first element in the query list. Query operators following the function are applied to the result of the function or, if the function is the last operator in the query, the function return value is collected in the query-document results list.

Examples:

;; Secret Agent document to query
(setf *agents* '((:control
                  (((:name . "Max") (:agent . 86)) 
                   ((:name . "Ninety Nine") (:agent . 99))
                   ((:agent . 44))
                   ((:agent . 13))
                   ((:name . "Hymie"))
                   ((:name . "Larrabee"))
                   ((:name . "Fang") (:agent . "K-9"))))
                 (:kaos
                  (((:name . "Siegfried"))
                   ((:name . "Shtarker"))
                   ((:name . "The Claw"))
                   ((:name . "Colonel von Klaus"))))))

;; Get the agent values for all Control agents that have these values
(query-document '(:control :agent) *agents*)
=> ("K-9" 13 44 99 86)

;; Get names of agents that work for Control or Kaos, for agents who have names
(query-document '(:* :name) *agents*)
=> ("Colonel von Klaus" "The Claw" "Shtarker" "Siegfried" "Fang" "Larrabee" 
  "Hymie" "Ninety Nine" "Max")

;; Get a value from a deeply nested position
(query-document '(:** :value) '((:a . 
                                 ((:very . 
                                   ((:deeply . 
                                     ((:nested . 
                                       ((:value . "Deep Value")))))))))))
=> ("Deep Value")

;; Use the recursive wild card operator to retrieve values from any level
(query-document '(:** :value) 
                '((:one . 
                   ((:very . 
                     ((:deeply . 
                       ((:nested . 
                         ((:value . "Deep Value")))))))))
                  (:this . 
                    ((:value . 
                      ((:is . 
                        ((:different . "Different Deep Value")))))))))

=> (((:IS (:DIFFERENT . "Different Deep Value"))) "Deep Value")

;; Functions
;;
;; Query the results of (get-all-documents), extracting all
;; document :|id| values in the :|rows| property.
;; 
;; Use the clouchdb function (get-document) to look up the 
;; corresponding document for each value of :|id|. The form below will 
;; return the value of the :name property for all documents in the
;; current database that have one or more :name properties

(query-document `(:|rows| :|id| ,#'get-document :** :name) (get-all-documents))

See the function (example3) in examples.lisp for more information

[Function]
set-document-property doc &rest args

Modify existing named property or add property names and values to a document. This function returns a new copy of the specified document, it does not modify its document parameter.

Example:

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

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

;; Modify existing :name value and add :employer property
(put-document 
  (set-document-property (get-document "max")
                         :name "Maxwell Smart, Secret Agent"
                         :employer "Control"))
(get-document "max")
=> ((:|_id| . "max") (:|_rev| . "655510103") (:EMPLOYER . "Control")
    (:NAME . "Maxwell Smart, Secret Agent") (:AGENT . 86))

Views API

Views are the query mechanism for 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. While it is possible to use JavaScript with the Clouchdb API, it may be more natural to use the Parenscript library instead. Parenscript is a library for generating JavaScript using a Lisp syntax.

Note that CouchDb and JavaScript are case sensitivie. Field names in documents created using un-quoted keyword symbol field names (e.g., :name) will be converted to upper case by Lisp and result in upper case field names in CouchDb and JavaScript.

Field Name Case Comparison
Clouchdb Parenscript CouchDb/JavaScript
:name, :NAME, :NaMe *name*, *NAME* *NaMe* NAME
:|name| name name
:|mixedCase| mixed-case mixedCase

Please refer to CouchDb View API Documentation for general information about CouchDb views. Note: Many details of CouchDb views have yet to be documented. In the meantime, see this blog post for some useful hints.

[Function]
ad-hoc-view view &key key start-key start-key-docid end-key count update descending skip

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

Keyword parameters

Example:

(create-document '((:name . "Larrabee")))

(ad-hoc-view "{'map': function(doc) { 
                        if (doc.NAME == 'Larrabee') {
                          map(null,doc.NAME) 
                        }
                      };
              }")

Ok, but JavaScript looks exotic and scary. The following Parenscript expression generates the same ad-hoc view as above, but with a more comfortable syntax:

(create-document '((:name . "Larrabee")))

(ad-hoc-view (ps-view ()
               (defun map (doc)
                 (with-slots (*name*) doc
                   (if (eql *name* "Larrabee")
                     (map nil doc))))))

Note that views may be defined which refer to document properties which may not exist in all documents in the database. For example, given the view above CouchDb will simply ignore documents that do not contain a NAME property.

Because ParenScript also supports Object-Oriented style "dot" notation, the (with-slots) expression can be replaced with the following:

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

See (create-ps-view) and Example 3

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

Creates a view document containing one or more view definitions. The view definitions may be specified in a JavaScript string or Parenscript s-expresions.

The following document, taken from the CouchDb HttpViewAPI Wiki, is an example of a native JavaScript CouchDb View document that defines three view components "all", "by_lastname", and "total_purchases":

{
  "_id":"_design/company",
  "_rev":"12345",
  "language": "javascript",
  "views":
  {
    "all": {
      "map": "function(doc) { if (doc.Type == 'customer')  emit(null, doc) }"
    },
    "by_lastname": {
      "map": "function(doc) { if (doc.Type == 'customer')  emit(doc.LastName, doc) }"
    },
    "total_purchases": {
      "map": "function(doc) { if (doc.Type == 'purchase')  
                emit(doc.Customer, doc.Amount) }",
      "reduce": "function(keys, values) { return sum(values) }"
    }
  }
}

The example view document above can be created in ClouchDb using Parenscript to generate JavaScript as follows

(create-ps-view "company"
  (ps-view ("all")
    (defun map (doc)
      (with-slots (type) doc
        (if (eql "customer" type)
          (emit null doc)))))
  (ps-view ("by_lastname")
    (defun map (doc)
      (with-slots (type last-name) doc
        (if (eql type "customer")
          (emit last-name doc)))))
  (ps-view ("total_purchases")
    (defun map (doc)
      (with-slots (type customer amount) doc
        (if (eql type "purchase")
          (emit customer amount)))
    (defun reduce (keys values)
      (return (sum values)))))

Note that the defuns in the example above define JavaScript functions, not Lisp functions, and that there are some capitization differences between the JavaScript and ParenScript view documents.

See (ps-view) (invoke-view) (ad-hoc-view) and Example 3

[Function]
delete-view id &key rev

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

See (create-ps-view)

[Macro]
ps-view &optional view-name

Use Parenscript to generate view JavaScript with an optional view-name. This macro can be used within both the ad-hoc-view and invoke-view methods as an alternative to supplying the JavaScript view definition in strings.

Only functions map or reduce may be defined, as these are the only functions supported by CouchDb. The map function is required, reduce is optional.

This macro can be evaulated in Lisp to view the JavaScript product as a string.

Optional parameters

Example:

;; Views defined with Parenscript
(create-ps-view "names"
  (ps-view ("larrabee")
    (defun map (doc)  ;; parameter-less view
      (with-slots (name) doc
        (if (eql "Larrabee" name)
          (emit nil doc)))))
  (ps-view ("name")
    (defun map (doc)  ;; parameter view
      (with-slots (name) doc
        (emit name doc)))))

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

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

See (create-ps-view) (invoke-view) (ad-hoc-view) and Example 3

[Function]
invoke-view id view &key key start-key start-key-docid end-key count update descending skip

Invoke specified view in identified view document.

Keyword parameters

Example:

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

;; Views defined with Parenscript
(create-ps-view "names"
  (ps-view ("larrabee")
    (defun map (doc)  ;; parameter-less view
      (with-slots (name) doc
        (if (eql "Larrabee" name)
          (emit nil doc)))))
  (ps-view ("name")
    (defun map (doc)  ;; parameter view
      (with-slots (name) doc
        (emit name doc)))))

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

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

See (create-ps-view) (ps-view) (ad-hoc-view) and Example 3

Symbol Index

ad-hoc-view
as-field-name-string
as-keyword-symbol
compact-db
create-db
create-document
create-ps-view
*document-fetch-fn*
*document-update-fn*
delete-db
delete-document
delete-view
document-property
get-all-documents
get-db-info
get-document
invoke-view
list-dbs
post-document
ps-view
put-document
query-document
set-connection
set-document-property
with-connection

Issues and Bugs

CouchDb is a young database server, it is currently in development and not yet feature complete nor are some aspects of its functionality even fully designed or finalized yet. Clouchdb is a new library supporting this database, and it too is still in development. As a result, changes to and bugs in the Clouchdb API should not be unexpected, though I do strive to minimise both.

With that in mind, any problems not outlined below should be brought to the author's attention through the clouchdb-devel email list.

Currently known issues:

Back to Common-lisp.net.

Valid XHTML 1.0 Strict

Valid XHTML 1.0 Strict