Skip to content

Commit b4dca14

Browse files
committed
Add a Database.save method for creating and updating a doc.
1 parent f6b40bb commit b4dca14

File tree

2 files changed

+71
-0
lines changed

2 files changed

+71
-0
lines changed

couchdb/client.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,40 @@ def create(self, data):
357357
_, _, data = self.resource.post_json(body=data)
358358
return data['id']
359359

360+
def save(self, doc, **options):
361+
"""Create a new document or update an existing document.
362+
363+
If doc has no _id then the server will allocate a random ID and a new
364+
document will be created. Otherwise the doc's _id will be used to
365+
identity the document to create or update. Trying to update an existing
366+
document with an incorrect _rev will raise a ResourceConflict exception.
367+
368+
Note that it is generally better to avoid saving documents with no _id
369+
and instead generate document IDs on the client side. This is due to
370+
the fact that the underlying HTTP ``POST`` method is not idempotent,
371+
and an automatic retry due to a problem somewhere on the networking
372+
stack may cause multiple documents being created in the database.
373+
374+
To avoid such problems you can generate a UUID on the client side.
375+
Python (since version 2.5) comes with a ``uuid`` module that can be
376+
used for this::
377+
378+
from uuid import uuid4
379+
doc = {'_id': uuid4().hex, 'type': 'person', 'name': 'John Doe'}
380+
db.save(doc)
381+
382+
:param doc: the document to store
383+
:param options: optional args, e.g. batch='ok'
384+
:return: (id, rev) tuple of the save document
385+
:rtype: `tuple`
386+
"""
387+
_, _, data = self.resource.post_json(body=doc, **options)
388+
id, rev = data['id'], data.get('rev')
389+
doc['_id'] = id
390+
if rev is not None: # Not present for batch='ok'
391+
doc['_rev'] = rev
392+
return id, rev
393+
360394
def commit(self):
361395
"""If the server is configured to delay commits, or previous requests
362396
used the special ``X-Couch-Full-Commit: false`` header to disable

couchdb/tests/client.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,43 @@ def tearDown(self):
141141

142142
class DatabaseTestCase(TempDatabaseMixin, unittest.TestCase):
143143

144+
def test_save_new(self):
145+
doc = {'foo': 'bar'}
146+
id, rev = self.db.save(doc)
147+
self.assertTrue(id is not None)
148+
self.assertTrue(rev is not None)
149+
self.assertEqual((id, rev), (doc['_id'], doc['_rev']))
150+
doc = self.db.get(id)
151+
self.assertEqual(doc['foo'], 'bar')
152+
153+
def test_save_new_with_id(self):
154+
doc = {'_id': 'foo'}
155+
id, rev = self.db.save(doc)
156+
self.assertTrue(doc['_id'] == id == 'foo')
157+
self.assertEqual(doc['_rev'], rev)
158+
159+
def test_save_existing(self):
160+
doc = {}
161+
id_rev_old = self.db.save(doc)
162+
doc['foo'] = True
163+
id_rev_new = self.db.save(doc)
164+
self.assertTrue(doc['_rev'] == id_rev_new[1])
165+
self.assertTrue(id_rev_old[1] != id_rev_new[1])
166+
167+
def test_save_new_batch(self):
168+
doc = {'_id': 'foo'}
169+
id, rev = self.db.save(doc, batch='ok')
170+
self.assertTrue(rev is None)
171+
self.assertTrue('_rev' not in doc)
172+
173+
def test_save_existing_batch(self):
174+
doc = {'_id': 'foo'}
175+
self.db.save(doc)
176+
id_rev_old = self.db.save(doc)
177+
id_rev_new = self.db.save(doc, batch='ok')
178+
self.assertTrue(id_rev_new[1] is None)
179+
self.assertEqual(id_rev_old[1], doc['_rev'])
180+
144181
def test_exists(self):
145182
self.assertTrue(client.Database(client.DEFAULT_BASE_URL+'python-tests'))
146183
self.assertFalse(client.Database('python-tests-missing'))

0 commit comments

Comments
 (0)