Merge lp:~cmiller/desktopcouch/excise-temporary-views into lp:desktopcouch

Proposed by Chad Miller
Status: Merged
Approved by: Joshua Blount
Approved revision: 25
Merged at revision: not available
Proposed branch: lp:~cmiller/desktopcouch/excise-temporary-views
Merge into: lp:desktopcouch
Diff against target: None lines
To merge this branch: bzr merge lp:~cmiller/desktopcouch/excise-temporary-views
Reviewer Review Type Date Requested Status
Joshua Blount (community) Approve
Stuart Langridge (community) Approve
Review via email: mp+9471@code.launchpad.net

Commit message

Remove temporary-view function and add support for permanent views.

To post a comment you must log in.
25. By Chad Miller

Better wording in records docs.

Revision history for this message
Stuart Langridge (sil) wrote :

Looks good to me, and doctests pass afaict.

review: Approve
Revision history for this message
Joshua Blount (jblount) wrote :

Looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'desktopcouch/records/doc/records.txt'
2--- desktopcouch/records/doc/records.txt 2009-07-08 17:48:11 +0000
3+++ desktopcouch/records/doc/records.txt 2009-07-30 16:13:07 +0000
4@@ -29,11 +29,34 @@
5 b
6 >>>
7
8-You can use Couch's ad-hoc query functionality:
9-
10->>> rows = db.query('''function(doc) { emit(doc._id, null) }''')
11->>> print list(rows)[0].key == record_id
12+There is no ad-hoc query functionality.
13+
14+For views, you should specify a design document for most all calls.
15+>>> design_doc = "application"
16+
17+To create a view:
18+
19+>>> map_js = """function(doc) { emit(doc._id, null) }"""
20+>>> reduce_js = None
21+>>> db.add_view("blueberries", map_js, reduce_js, design_doc)
22+
23+List views for a given design document:
24+>>> db.list_views(design_doc)
25+['blueberries']
26+
27+Test that a view exists:
28+>>> db.view_exists("blueberries", design_doc)
29 True
30->>>
31-
32-
33+
34+Execute a view. Results from execute_view() take list-like syntax to pick one
35+or more rows to retreive. Use index or slice notation.
36+>>> result = db.execute_view("blueberries", design_doc)
37+>>> for row in result["idfoo"]:
38+... pass # all rows with id "idfoo". Unlike lists, may be more than one.
39+
40+Finally, remove a view. It returns a dict containing the deleted view data.
41+>>> db.delete_view("blueberries", design_doc)
42+{'map': 'function(doc) { emit(doc._id, null) }'}
43+
44+For most nonpredicate view operations, if the view you ask for does not exist,
45+it will throw a KeyError exception.
46
47=== modified file 'desktopcouch/records/server.py'
48--- desktopcouch/records/server.py 2009-07-28 04:12:18 +0000
49+++ desktopcouch/records/server.py 2009-07-30 15:57:52 +0000
50@@ -17,15 +17,21 @@
51 # Authors: Eric Casteleijn <eric.casteleijn@canonical.com>
52 # Mark G. Saye <mark.saye@canonical.com>
53 # Stuart Langridge <stuart.langridge@canonical.com>
54+# Chad Miller <chad.miller@canonical.com>
55
56 """The Desktop Couch Records API."""
57
58 from couchdb import Server
59 from couchdb.client import ResourceNotFound, ResourceConflict
60+from couchdb.design import ViewDefinition
61 import desktopcouch
62 from desktopcouch.records.record import Record
63
64
65+#DEFAULT_DESIGN_DOCUMENT = "design"
66+DEFAULT_DESIGN_DOCUMENT = None # each view in its own eponymous design doc.
67+
68+
69 class NoSuchDatabase(Exception):
70 "Exception for trying to use a non-existent database"
71
72@@ -57,9 +63,9 @@
73 self.db = self._server[database]
74 self.record_factory = record_factory or Record
75
76- def query(self, map_fun, reduce_fun=None, language='javascript',
77- wrapper=None, **options):
78- """Pass-through to CouchDB query"""
79+ def _temporary_query(self, map_fun, reduce_fun=None, language='javascript',
80+ wrapper=None, **options):
81+ """Pass-through to CouchDB library. Deprecated."""
82 return self.db.query(map_fun, reduce_fun, language,
83 wrapper, **options)
84
85@@ -112,11 +118,131 @@
86 self.db[record_id] = record
87
88 def record_exists(self, record_id):
89- """Check if record with given id exists"""
90+ """Check if record with given id exists."""
91 if record_id not in self.db:
92 return False
93 record = self.db[record_id]
94 return 'deleted' not in record.get('application_annotations', {}).get(
95 'Ubuntu One', {}).get('private_application_annotations', {})
96
97-
98+ def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
99+ """Remove a view, given its name. Raises a KeyError on a unknown
100+ view. Returns a dict of functions the deleted view defined."""
101+ if design_doc is None:
102+ design_doc = view_name
103+
104+ doc_id = "_design/%(design_doc)s" % locals()
105+
106+ # No atomic updates. Only read & mutate & write. Le sigh.
107+ # First, get current contents.
108+ view_container = self.db.get(doc_id, {'_id': doc_id })["views"]
109+
110+ deleted_data = view_container.pop(view_name) # Remove target
111+
112+ if len(view_container) > 0:
113+ # Construct a new list of objects representing all views to have.
114+ views = [
115+ ViewDefinition(design_doc, k, v.get("map"), v.get("reduce"))
116+ for k, v
117+ in view_container.iteritems()
118+ ]
119+ # Push back a new batch of view. Pray to Eris that this doesn't
120+ # clobber anything we want.
121+
122+ # sync_many does nothing if we pass an empty list. It even gets
123+ # its design-document from the ViewDefinition items, and if there
124+ # are no items, then it has no idea of a design document to
125+ # update. This is a serious flaw. Thus, the "else" to follow.
126+ ViewDefinition.sync_many(self.db, views, remove_missing=True)
127+ else:
128+ # There are no views left in this design document.
129+
130+ # Remove design document. This assumes there are only views in
131+ # design documents. :(
132+ del self.db[doc_id]
133+
134+ assert not self.view_exists(view_name, design_doc)
135+
136+ return deleted_data
137+
138+ def execute_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
139+ """Execute view and return results."""
140+ if design_doc is None:
141+ design_doc = view_name
142+
143+ view_id_fmt = "_design/%(design_doc)s/_view/%(view_name)s"
144+ return self.db.view(view_id_fmt % locals())
145+
146+ def add_view(self, view_name, map_js, reduce_js,
147+ design_doc=DEFAULT_DESIGN_DOCUMENT):
148+ """Create a view, given a name and the two parts (map and reduce).
149+ Return the document id."""
150+ if design_doc is None:
151+ design_doc = view_name
152+
153+ view = ViewDefinition(design_doc, view_name, map_js, reduce_js)
154+ view.sync(self.db)
155+ assert self.view_exists(view_name, design_doc)
156+
157+ def view_exists(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
158+ """Does a view with a given name, in a optional design document
159+ exist?"""
160+ if design_doc is None:
161+ design_doc = view_name
162+
163+ doc_id = "_design/%(design_doc)s" % locals()
164+
165+ try:
166+ view_container = self.db.get(doc_id, {'_id': doc_id })["views"]
167+ return view_name in view_container
168+ except KeyError:
169+ return False
170+
171+ def list_views(self, design_doc):
172+ """Return a list of view names for a given design document. There is
173+ no error if the design document does not exist or if there are no views
174+ in it."""
175+ doc_id = "_design/%(design_doc)s" % locals()
176+ try:
177+ return list(self.db.get(doc_id, {'_id': doc_id })["views"])
178+ except KeyError:
179+ return []
180+
181+ def get_records_and_type(self, create_view=False,
182+ design_doc=DEFAULT_DESIGN_DOCUMENT):
183+ """A convenience function to get records. We optionally create a view
184+ in the design document. C<create_view> may be True or False, and a
185+ special value, None, is analogous to O_EXCL|O_CREAT .
186+
187+ Use the view to return *all* records. If there is no view to use or we
188+ insist on creating a new view and cannot, raise KeyError .
189+
190+ Use index notation on the result to get rows with a particular record
191+ type.
192+ =>> results = get_records_and_type()
193+ =>> for foo_document in results["foo"]:
194+ ... print foo_document
195+
196+ Use slice notation to apply start-key and end-key options to the view.
197+ =>> results = get_records_and_type()
198+ =>> people = results[['Person']:['Person','ZZZZ']]
199+ """
200+ view_name = "get_records_and_type"
201+ view_map_js = """function(doc) { emit(doc.record_type, doc) }"""
202+
203+ if design_doc is None:
204+ design_doc = view_name
205+
206+ exists = self.view_exists(view_name, design_doc)
207+
208+ if exists:
209+ if create_view is None:
210+ raise KeyError("Exclusive creation failed.")
211+ else:
212+ if create_view == False:
213+ raise KeyError("View doesn't already exist.")
214+
215+ if not exists:
216+ self.add_view(view_name, view_map_js, None, design_doc)
217+
218+ return self.execute_view(view_name, design_doc)
219
220=== modified file 'desktopcouch/records/tests/test_server.py'
221--- desktopcouch/records/tests/test_server.py 2009-07-28 03:43:02 +0000
222+++ desktopcouch/records/tests/test_server.py 2009-07-30 15:57:52 +0000
223@@ -93,3 +93,70 @@
224 self.assertEqual(11, working_copy['field1'])
225 self.assertEqual(22, working_copy['field2'])
226 self.assertEqual(3, working_copy['field3'])
227+
228+ def test_view_add_and_delete(self):
229+ design_doc = "design"
230+ view1_name = "unit_tests_are_wonderful"
231+ view2_name = "unit_tests_are_marvelous"
232+ view3_name = "unit_tests_are_fantastic"
233+
234+ map_js = """function(doc) { emit(doc._id, null) }"""
235+ reduce_js = """\
236+ function (key, values, rereduce) {
237+ return sum(values);
238+ }"""
239+
240+ # add two and delete two.
241+ self.assertRaises(KeyError, self.database.delete_view, view1_name, design_doc)
242+ self.assertRaises(KeyError, self.database.delete_view, view2_name, design_doc)
243+ self.database.add_view(view1_name, map_js, reduce_js, design_doc)
244+ self.database.add_view(view2_name, map_js, reduce_js, design_doc)
245+ self.database.delete_view(view1_name, design_doc)
246+ self.assertRaises(KeyError, self.database.delete_view, view1_name, design_doc)
247+ self.database.delete_view(view2_name, design_doc)
248+ self.assertRaises(KeyError, self.database.delete_view, view2_name, design_doc)
249+
250+ def test_func_get_records_and_type(self):
251+ record_ids_we_care_about = set()
252+ good_record_type = "http://example.com/unittest/good"
253+ other_record_type = "http://example.com/unittest/bad"
254+
255+ for i in range(7):
256+ if i % 3 == 1:
257+ record = Record({'record_number': i},
258+ record_type=good_record_type)
259+ record_ids_we_care_about.add(self.database.put_record(record))
260+ else:
261+ record = Record({'record_number': i},
262+ record_type=other_record_type)
263+ self.database.put_record(record)
264+
265+ results = self.database.get_records_and_type(create_view=True)
266+
267+ for row in results[good_record_type]: # index notation
268+ self.assertTrue(row.id in record_ids_we_care_about)
269+ record_ids_we_care_about.remove(row.id)
270+
271+ self.assertTrue(len(record_ids_we_care_about) == 0, "expected zero")
272+
273+ def test_list_views(self):
274+ design_doc = "d"
275+ self.assertEqual(self.database.list_views(design_doc), [])
276+
277+ view_name = "unit_tests_are_fantastic"
278+ map_js = """function(doc) { emit(doc._id, null) }"""
279+ self.database.add_view(view_name, map_js, None, design_doc)
280+
281+ self.assertEqual(self.database.list_views(design_doc), [view_name])
282+ self.database.delete_view(view_name, design_doc)
283+
284+ self.assertEqual(self.database.list_views(design_doc), [])
285+
286+ def test_get_view_by_type_new_but_already(self):
287+ self.database.get_records_and_type(create_view=True)
288+ self.database.get_records_and_type(create_view=True)
289+ # No exceptions on second run? Yay.
290+
291+ def test_get_view_by_type_createxcl_fail(self):
292+ self.database.get_records_and_type(create_view=True)
293+ self.assertRaises(KeyError, self.database.get_records_and_type, create_view=None)

Subscribers

People subscribed via source and target branches