Merge lp:~thisfred/u1db/its-full-of-conflicts into lp:u1db

Proposed by Eric Casteleijn on 2012-08-09
Status: Merged
Approved by: Eric Casteleijn on 2012-08-13
Approved revision: 381
Merged at revision: 372
Proposed branch: lp:~thisfred/u1db/its-full-of-conflicts
Merge into: lp:u1db
Diff against target: 661 lines (+251/-89)
9 files modified
doc/sqlite_schema.txt (+2/-2)
src/u1db.c (+7/-2)
src/u1db_query.c (+2/-2)
u1db/backends/__init__.py (+4/-6)
u1db/backends/inmemory.py (+13/-11)
u1db/backends/sqlite_backend.py (+75/-52)
u1db/tests/test_backends.py (+32/-0)
u1db/tests/test_sqlite_backend.py (+108/-12)
u1db/tests/test_sync.py (+8/-2)
To merge this branch: bzr merge lp:~thisfred/u1db/its-full-of-conflicts
Reviewer Review Type Date Requested Status
John O'Brien (community) 2012-08-09 Approve on 2012-08-13
Review via email: mp+119057@code.launchpad.net

Commit Message

- refactored the way sql queries are built in the python backend, and brought it more into step with the C implementation.
- made get_all_docs, get_from_index and get_range_from_index conflict aware

Description of the Change

- refactored the way sql queries are built in the python backend, and brought it more into step with the C implementation.
- made get_all_docs, get_from_index and get_range_from_index conflict aware

To post a comment you must log in.
380. By Eric Casteleijn on 2012-08-09

clarified?

381. By Eric Casteleijn on 2012-08-10

no need to cast int

John O'Brien (jdobrien) wrote :

the queries look sane to me :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'doc/sqlite_schema.txt'
2--- doc/sqlite_schema.txt 2012-07-16 16:20:53 +0000
3+++ doc/sqlite_schema.txt 2012-08-10 20:11:19 +0000
4@@ -397,8 +397,8 @@
5 database. We timed a loop that extracted every document from the database,
6 one-at-a-time by calling ``get_doc()``. This also has the overhead of checking
7 if a given document has conflicts, etc. All currently implementations also
8-support ``_get_doc()`` which extracts the content without checking for
9-conflicts. Further, `Partial Expanded Fields`_ internally has a
10+support ``_get_doc()`` which extracts the content without (by default) checking
11+for conflicts. Further, `Partial Expanded Fields`_ internally has a
12 ``_iter_all_docs`` method that it uses when it wants to update the index data.
13 This simply streams out the document content, as quickly as SQLite can produce
14 it.
15
16=== modified file 'src/u1db.c'
17--- src/u1db.c 2012-07-31 19:43:46 +0000
18+++ src/u1db.c 2012-08-10 20:11:19 +0000
19@@ -1208,6 +1208,7 @@
20 void *context, u1db_doc_callback cb)
21 {
22 int status;
23+ int conflicts;
24 sqlite3_stmt *statement;
25
26 if (db == NULL || cb == NULL) {
27@@ -1217,7 +1218,10 @@
28 if (status != U1DB_OK)
29 return status;
30 status = sqlite3_prepare_v2(db->sql_handle,
31- "SELECT doc_id, doc_rev, content FROM document", -1, &statement, NULL);
32+ "SELECT document.doc_id, document.doc_rev, document.content, "
33+ "count(conflicts.doc_rev) FROM document LEFT OUTER JOIN conflicts ON "
34+ "conflicts.doc_id = document.doc_id GROUP BY document.doc_id, "
35+ "document.doc_rev, document.content", -1, &statement, NULL);
36 if (status != SQLITE_OK) { goto finish; }
37 status = sqlite3_step(statement);
38 while (status == SQLITE_ROW) {
39@@ -1228,9 +1232,10 @@
40 doc_id = (char *)sqlite3_column_text(statement, 0);
41 revision = (char *)sqlite3_column_text(statement, 1);
42 content = (char *)sqlite3_column_text(statement, 2);
43+ conflicts = sqlite3_column_int(statement, 3);
44 if (content != NULL || include_deleted) {
45 status = u1db__allocate_document(
46- doc_id, revision, content, 0, &doc);
47+ doc_id, revision, content, conflicts > 0, &doc);
48 if (status != U1DB_OK)
49 goto finish;
50 cb(context, doc);
51
52=== modified file 'src/u1db_query.c'
53--- src/u1db_query.c 2012-07-27 19:06:03 +0000
54+++ src/u1db_query.c 2012-08-10 20:11:19 +0000
55@@ -747,7 +747,7 @@
56 // We use u1db_get_docs so we can pass check_for_conflicts=0, which is
57 // currently expected by the test suite.
58 status = u1db_get_docs(
59- db, 1, (const char**)&doc_id, 0, 0, context, cb);
60+ db, 1, (const char**)&doc_id, 1, 0, context, cb);
61 if (status != U1DB_OK) { goto finish; }
62 status = sqlite3_step(statement);
63 }
64@@ -848,7 +848,7 @@
65 // We use u1db_get_docs so we can pass check_for_conflicts=0, which is
66 // currently expected by the test suite.
67 status = u1db_get_docs(
68- db, 1, (const char**)&doc_id, 0, 0, context, cb);
69+ db, 1, (const char**)&doc_id, 1, 0, context, cb);
70 if (status != U1DB_OK) { goto finish; }
71 status = sqlite3_step(statement);
72 }
73
74=== modified file 'u1db/backends/__init__.py'
75--- u1db/backends/__init__.py 2012-07-26 19:14:26 +0000
76+++ u1db/backends/__init__.py 2012-08-10 20:11:19 +0000
77@@ -73,11 +73,10 @@
78 """
79 raise NotImplementedError(self._get_generation_info)
80
81- def _get_doc(self, doc_id):
82+ def _get_doc(self, doc_id, check_for_conflicts=False):
83 """Extract the document from storage.
84
85- This can return None if the document doesn't exist, it should not check
86- if there are any conflicts, etc.
87+ This can return None if the document doesn't exist.
88 """
89 raise NotImplementedError(self._get_doc)
90
91@@ -110,11 +109,10 @@
92 def get_docs(self, doc_ids, check_for_conflicts=True,
93 include_deleted=False):
94 for doc_id in doc_ids:
95- doc = self._get_doc(doc_id)
96+ doc = self._get_doc(
97+ doc_id, check_for_conflicts=check_for_conflicts)
98 if doc.is_tombstone() and not include_deleted:
99 continue
100- if check_for_conflicts:
101- doc.has_conflicts = self._has_conflicts(doc_id)
102 yield doc
103
104 def _get_trans_id_for_gen(self, generation):
105
106=== modified file 'u1db/backends/inmemory.py'
107--- u1db/backends/inmemory.py 2012-07-13 19:35:51 +0000
108+++ u1db/backends/inmemory.py 2012-08-10 20:11:19 +0000
109@@ -100,9 +100,9 @@
110 raise errors.InvalidDocId()
111 self._check_doc_id(doc.doc_id)
112 self._check_doc_size(doc)
113- if self._has_conflicts(doc.doc_id):
114+ old_doc = self._get_doc(doc.doc_id, check_for_conflicts=True)
115+ if old_doc and old_doc.has_conflicts:
116 raise errors.ConflictedDoc()
117- old_doc = self._get_doc(doc.doc_id)
118 if old_doc and doc.rev is None and old_doc.is_tombstone():
119 new_rev = self._allocate_doc_rev(old_doc.rev)
120 else:
121@@ -127,23 +127,25 @@
122 self._docs[doc.doc_id] = (doc.rev, doc.get_json())
123 self._transaction_log.append((doc.doc_id, trans_id))
124
125- def _get_doc(self, doc_id):
126+ def _get_doc(self, doc_id, check_for_conflicts=False):
127 try:
128 doc_rev, content = self._docs[doc_id]
129 except KeyError:
130 return None
131- return self._factory(doc_id, doc_rev, content)
132+ doc = self._factory(doc_id, doc_rev, content)
133+ if check_for_conflicts:
134+ doc.has_conflicts = (doc.doc_id in self._conflicts)
135+ return doc
136
137 def _has_conflicts(self, doc_id):
138 return doc_id in self._conflicts
139
140 def get_doc(self, doc_id, include_deleted=False):
141- doc = self._get_doc(doc_id)
142+ doc = self._get_doc(doc_id, check_for_conflicts=True)
143 if doc is None:
144 return None
145 if doc.is_tombstone() and not include_deleted:
146 return None
147- doc.has_conflicts = (doc.doc_id in self._conflicts)
148 return doc
149
150 def get_all_docs(self, include_deleted=False):
151@@ -153,7 +155,9 @@
152 for doc_id, (doc_rev, content) in self._docs.items():
153 if content is None and not include_deleted:
154 continue
155- results.append(self._factory(doc_id, doc_rev, content))
156+ doc = self._factory(doc_id, doc_rev, content)
157+ doc.has_conflicts = self._has_conflicts(doc_id)
158+ results.append(doc)
159 return (generation, results)
160
161 def get_doc_conflicts(self, doc_id):
162@@ -249,8 +253,7 @@
163 doc_ids = index.lookup(key_values)
164 result = []
165 for doc_id in doc_ids:
166- doc_rev, doc = self._docs[doc_id]
167- result.append(self._factory(doc_id, doc_rev, doc))
168+ result.append(self._get_doc(doc_id, check_for_conflicts=True))
169 return result
170
171 def get_range_from_index(self, index_name, start_value=None,
172@@ -267,8 +270,7 @@
173 doc_ids = index.lookup_range(start_value, end_value)
174 result = []
175 for doc_id in doc_ids:
176- doc_rev, doc = self._docs[doc_id]
177- result.append(self._factory(doc_id, doc_rev, doc))
178+ result.append(self._get_doc(doc_id, check_for_conflicts=True))
179 return result
180
181 def get_index_keys(self, index_name):
182
183=== modified file 'u1db/backends/sqlite_backend.py'
184--- u1db/backends/sqlite_backend.py 2012-08-01 16:41:14 +0000
185+++ u1db/backends/sqlite_backend.py 2012-08-10 20:11:19 +0000
186@@ -292,16 +292,27 @@
187 " ORDER BY generation")
188 return c.fetchall()
189
190- def _get_doc(self, doc_id):
191+ def _get_doc(self, doc_id, check_for_conflicts=False):
192 """Get just the document content, without fancy handling."""
193 c = self._db_handle.cursor()
194- c.execute("SELECT doc_rev, content FROM document WHERE doc_id = ?",
195- (doc_id,))
196+ if check_for_conflicts:
197+ c.execute(
198+ "SELECT document.doc_rev, document.content, "
199+ "count(conflicts.doc_rev) FROM document LEFT OUTER JOIN "
200+ "conflicts ON conflicts.doc_id = document.doc_id WHERE "
201+ "document.doc_id = ? GROUP BY document.doc_id, "
202+ "document.doc_rev, document.content;", (doc_id,))
203+ else:
204+ c.execute(
205+ "SELECT doc_rev, content, 0 FROM document WHERE doc_id = ?",
206+ (doc_id,))
207 val = c.fetchone()
208 if val is None:
209 return None
210- doc_rev, content = val
211- return self._factory(doc_id, doc_rev, content)
212+ doc_rev, content, conflicts = val
213+ doc = self._factory(doc_id, doc_rev, content)
214+ doc.has_conflicts = conflicts > 0
215+ return doc
216
217 def _has_conflicts(self, doc_id):
218 c = self._db_handle.cursor()
219@@ -314,13 +325,11 @@
220 return True
221
222 def get_doc(self, doc_id, include_deleted=False):
223- doc = self._get_doc(doc_id)
224+ doc = self._get_doc(doc_id, check_for_conflicts=True)
225 if doc is None:
226 return None
227 if doc.is_tombstone() and not include_deleted:
228 return None
229- # TODO: A doc which appears deleted could still have conflicts...
230- doc.has_conflicts = self._has_conflicts(doc.doc_id)
231 return doc
232
233 def get_all_docs(self, include_deleted=False):
234@@ -328,12 +337,18 @@
235 generation = self._get_generation()
236 results = []
237 c = self._db_handle.cursor()
238- c.execute("SELECT doc_id, doc_rev, content FROM document;")
239+ c.execute(
240+ "SELECT document.doc_id, document.doc_rev, document.content, "
241+ "count(conflicts.doc_rev) FROM document LEFT OUTER JOIN conflicts "
242+ "ON conflicts.doc_id = document.doc_id GROUP BY document.doc_id, "
243+ "document.doc_rev, document.content;")
244 rows = c.fetchall()
245- for doc_id, doc_rev, content in rows:
246+ for doc_id, doc_rev, content, conflicts in rows:
247 if content is None and not include_deleted:
248 continue
249- results.append(self._factory(doc_id, doc_rev, content))
250+ doc = self._factory(doc_id, doc_rev, content)
251+ doc.has_conflicts = conflicts > 0
252+ results.append(doc)
253 return (generation, results)
254
255 def put_doc(self, doc):
256@@ -342,9 +357,9 @@
257 self._check_doc_id(doc.doc_id)
258 self._check_doc_size(doc)
259 with self._db_handle:
260- if self._has_conflicts(doc.doc_id):
261+ old_doc = self._get_doc(doc.doc_id, check_for_conflicts=True)
262+ if old_doc and old_doc.has_conflicts:
263 raise errors.ConflictedDoc()
264- old_doc = self._get_doc(doc.doc_id)
265 if old_doc and doc.rev is None and old_doc.is_tombstone():
266 new_rev = self._allocate_doc_rev(old_doc.rev)
267 else:
268@@ -429,14 +444,14 @@
269
270 def delete_doc(self, doc):
271 with self._db_handle:
272- old_doc = self._get_doc(doc.doc_id)
273+ old_doc = self._get_doc(doc.doc_id, check_for_conflicts=True)
274 if old_doc is None:
275 raise errors.DocumentDoesNotExist
276 if old_doc.rev != doc.rev:
277 raise errors.RevisionConflict()
278 if old_doc.is_tombstone():
279 raise errors.DocumentAlreadyDeleted
280- if self._has_conflicts(doc.doc_id):
281+ if old_doc.has_conflicts:
282 raise errors.ConflictedDoc()
283 new_rev = self._allocate_doc_rev(doc.rev)
284 doc.rev = new_rev
285@@ -588,22 +603,12 @@
286 return fields
287
288 @staticmethod
289- def _transform_glob(value, escape_char='.'):
290- """Transform the given glob value into a valid LIKE statement."""
291- to_escape = [escape_char, '%', '_']
292- for esc in to_escape:
293- value = value.replace(esc, escape_char + esc)
294- assert value[-1] == '*'
295- return value[:-1] + '%'
296-
297- @staticmethod
298 def _strip_glob(value):
299 """Remove the trailing * from a value."""
300 assert value[-1] == '*'
301 return value[:-1]
302
303- def get_from_index(self, index_name, *key_values):
304- definition = self._get_index_definition(index_name)
305+ def _format_query(self, definition, key_values):
306 # First, build the definition. We join the document_fields table
307 # against itself, as many times as the 'width' of our definition.
308 # We then do a query for each key_value, one-at-a-time.
309@@ -619,9 +624,8 @@
310 + (" AND d%d.value = ?" % (i,))
311 for i in range(len(definition))]
312 like_where = [novalue_where[i]
313- + (" AND d%d.value LIKE ? ESCAPE '.'" % (i,))
314+ + (" AND d%d.value GLOB ?" % (i,))
315 for i in range(len(definition))]
316- c = self._db_handle.cursor()
317 is_wildcard = False
318 # Merge the lists together, so that:
319 # [field1, field2, field3], [val1, val2, val3]
320@@ -629,8 +633,6 @@
321 # (field1, val1, field2, val2, field3, val3)
322 args = []
323 where = []
324- if len(key_values) != len(definition):
325- raise errors.InvalidValueForIndex()
326 for idx, (field, value) in enumerate(zip(definition, key_values)):
327 args.append(field)
328 if value.endswith('*'):
329@@ -643,7 +645,7 @@
330 # another wildcard
331 raise errors.InvalidGlobbing
332 where.append(like_where[idx])
333- args.append(self._transform_glob(value))
334+ args.append(value)
335 is_wildcard = True
336 else:
337 if is_wildcard:
338@@ -651,23 +653,33 @@
339 where.append(exact_where[idx])
340 args.append(value)
341 statement = (
342- "SELECT d.doc_id, d.doc_rev, d.content FROM document d, %s "
343- "WHERE %s ORDER BY %s;" % (
344- ', '.join(tables), ' AND '.join(where),
345- ', '.join(
346- ['d%d.value' % i for i in range(len(definition))])))
347+ "SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM "
348+ "document d, %s LEFT OUTER JOIN conflicts c ON c.doc_id = "
349+ "d.doc_id WHERE %s GROUP BY d.doc_id, d.doc_rev, d.content ORDER "
350+ "BY %s;" % (', '.join(tables), ' AND '.join(where), ', '.join(
351+ ['d%d.value' % i for i in range(len(definition))])))
352+ return statement, args
353+
354+ def get_from_index(self, index_name, *key_values):
355+ definition = self._get_index_definition(index_name)
356+ if len(key_values) != len(definition):
357+ raise errors.InvalidValueForIndex()
358+ statement, args = self._format_query(definition, key_values)
359+ c = self._db_handle.cursor()
360 try:
361 c.execute(statement, tuple(args))
362 except dbapi2.OperationalError, e:
363 raise dbapi2.OperationalError(str(e) +
364 '\nstatement: %s\nargs: %s\n' % (statement, args))
365 res = c.fetchall()
366- return [self._factory(r[0], r[1], r[2]) for r in res]
367+ results = []
368+ for row in res:
369+ doc = self._factory(row[0], row[1], row[2])
370+ doc.has_conflicts = row[3] > 0
371+ results.append(doc)
372+ return results
373
374- def get_range_from_index(self, index_name, start_value=None,
375- end_value=None):
376- """Return all documents with key values in the specified range."""
377- definition = self._get_index_definition(index_name)
378+ def _format_range_query(self, definition, start_value, end_value):
379 tables = ["document_fields d%d" % i for i in range(len(definition))]
380 novalue_where = [
381 "d.doc_id = d%d.doc_id AND d%d.field_name = ?" % (i, i) for i in
382@@ -677,9 +689,8 @@
383 range(len(definition))]
384 like_where = [
385 novalue_where[i] + (
386- " AND (d%d.value < ? OR d%d.value LIKE ? ESCAPE '.')" %
387- (i, i))
388- for i in range(len(definition))]
389+ " AND (d%d.value < ? OR d%d.value GLOB ?)" % (i, i)) for i in
390+ range(len(definition))]
391 range_where_lower = [
392 novalue_where[i] + (" AND d%d.value >= ?" % (i,)) for i in
393 range(len(definition))]
394@@ -732,7 +743,7 @@
395 raise errors.InvalidGlobbing
396 where.append(like_where[idx])
397 args.append(self._strip_glob(value))
398- args.append(self._transform_glob(value))
399+ args.append(value)
400 is_wildcard = True
401 else:
402 if is_wildcard:
403@@ -740,12 +751,19 @@
404 where.append(range_where_upper[idx])
405 args.append(value)
406 statement = (
407- "SELECT d.doc_id, d.doc_rev, d.content FROM document d, %s "
408- "WHERE %s ORDER BY %s;" % (
409- ', '.join(tables),
410- ' AND '.join(where),
411- ', '.join(
412- ['d%d.value' % i for i in range(len(definition))])))
413+ "SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM "
414+ "document d, %s LEFT OUTER JOIN conflicts c ON c.doc_id = "
415+ "d.doc_id WHERE %s GROUP BY d.doc_id, d.doc_rev, d.content ORDER "
416+ "BY %s;" % (', '.join(tables), ' AND '.join(where), ', '.join(
417+ ['d%d.value' % i for i in range(len(definition))])))
418+ return statement, args
419+
420+ def get_range_from_index(self, index_name, start_value=None,
421+ end_value=None):
422+ """Return all documents with key values in the specified range."""
423+ definition = self._get_index_definition(index_name)
424+ statement, args = self._format_range_query(
425+ definition, start_value, end_value)
426 c = self._db_handle.cursor()
427 try:
428 c.execute(statement, tuple(args))
429@@ -753,7 +771,12 @@
430 raise dbapi2.OperationalError(str(e) +
431 '\nstatement: %s\nargs: %s\n' % (statement, args))
432 res = c.fetchall()
433- return [self._factory(r[0], r[1], r[2]) for r in res]
434+ results = []
435+ for row in res:
436+ doc = self._factory(row[0], row[1], row[2])
437+ doc.has_conflicts = row[3] > 0
438+ results.append(doc)
439+ return results
440
441 def get_index_keys(self, index_name):
442 c = self._db_handle.cursor()
443
444=== modified file 'u1db/tests/test_backends.py'
445--- u1db/tests/test_backends.py 2012-08-01 17:07:36 +0000
446+++ u1db/tests/test_backends.py 2012-08-10 20:11:19 +0000
447@@ -667,6 +667,15 @@
448 self.assertEqual([alt_doc, doc],
449 self.db.get_doc_conflicts(doc.doc_id))
450
451+ def test_get_all_docs_sees_conflicts(self):
452+ doc = self.db.create_doc_from_json(simple_doc)
453+ alt_doc = self.make_document(doc.doc_id, 'alternate:1', nested_doc)
454+ self.db._put_doc_if_newer(
455+ alt_doc, save_conflict=True, replica_uid='r', replica_gen=1,
456+ replica_trans_id='foo')
457+ _, docs = self.db.get_all_docs()
458+ self.assertTrue(docs[0].has_conflicts)
459+
460 def test_get_doc_conflicts_unconflicted(self):
461 doc = self.db.create_doc_from_json(simple_doc)
462 self.assertEqual([], self.db.get_doc_conflicts(doc.doc_id))
463@@ -1124,6 +1133,18 @@
464 ('value', 'value2-3')],
465 sorted(self.db.get_index_keys('test-idx')))
466
467+ def test_get_from_index_sees_conflicts(self):
468+ doc = self.db.create_doc_from_json(simple_doc)
469+ self.db.create_index('test-idx', 'key', 'key2')
470+ alt_doc = self.make_document(
471+ doc.doc_id, 'alternate:1',
472+ '{"key": "value", "key2": ["value2-1", "value2-2", "value2-3"]}')
473+ self.db._put_doc_if_newer(
474+ alt_doc, save_conflict=True, replica_uid='r', replica_gen=1,
475+ replica_trans_id='foo')
476+ docs = self.db.get_from_index('test-idx', 'value', 'value2-1')
477+ self.assertTrue(docs[0].has_conflicts)
478+
479 def test_get_index_keys_multi_list_list(self):
480 self.db.create_doc_from_json(
481 '{"key": "value1-1 value1-2 value1-3", '
482@@ -1171,6 +1192,17 @@
483 [doc2, doc1, doc3],
484 self.db.get_range_from_index('test-idx', 'value2'))
485
486+ def test_get_range_from_index_sees_conflicts(self):
487+ doc = self.db.create_doc_from_json(simple_doc)
488+ self.db.create_index('test-idx', 'key')
489+ alt_doc = self.make_document(
490+ doc.doc_id, 'alternate:1', '{"key": "valuedepalue"}')
491+ self.db._put_doc_if_newer(
492+ alt_doc, save_conflict=True, replica_uid='r', replica_gen=1,
493+ replica_trans_id='foo')
494+ docs = self.db.get_range_from_index('test-idx', 'a')
495+ self.assertTrue(docs[0].has_conflicts)
496+
497 def test_get_range_from_index_end(self):
498 self.db.create_doc_from_json('{"key": "value3"}')
499 doc2 = self.db.create_doc_from_json('{"key": "value2"}')
500
501=== modified file 'u1db/tests/test_sqlite_backend.py'
502--- u1db/tests/test_sqlite_backend.py 2012-07-31 14:36:19 +0000
503+++ u1db/tests/test_sqlite_backend.py 2012-08-10 20:11:19 +0000
504@@ -360,18 +360,6 @@
505 self.assertRaises(errors.DatabaseDoesNotExist,
506 sqlite_backend.SQLiteDatabase.delete_database, path)
507
508- def assertTransform(self, sql_value, value):
509- transformed = sqlite_backend.SQLiteDatabase._transform_glob(value)
510- self.assertEqual(sql_value, transformed)
511-
512- def test_glob_escaping(self):
513- # SQL allows us to define any escape char we want, for now I'm just
514- # using '.'
515- self.assertTransform('val%', 'val*')
516- self.assertTransform('v.%al%', 'v%al*')
517- self.assertTransform('v._al%', 'v_al*')
518- self.assertTransform('v..al%', 'v.al*')
519-
520 def test__get_indexed_fields(self):
521 self.db.create_index('idx1', 'a', 'b')
522 self.assertEqual(set(['a', 'b']), self.db._get_indexed_fields())
523@@ -395,3 +383,111 @@
524 c.execute("SELECT doc_id, field_name, value FROM document_fields"
525 " ORDER BY doc_id, field_name, value")
526 self.assertEqual([(doc1.doc_id, 'key1', 'val1')], c.fetchall())
527+
528+ def assertFormatQueryEquals(self, exp_statement, exp_args, definition,
529+ values):
530+ statement, args = self.db._format_query(definition, values)
531+ self.assertEqual(exp_statement, statement)
532+ self.assertEqual(exp_args, args)
533+
534+ def test__format_query(self):
535+ self.assertFormatQueryEquals(
536+ "SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM "
537+ "document d, document_fields d0 LEFT OUTER JOIN conflicts c ON "
538+ "c.doc_id = d.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name "
539+ "= ? AND d0.value = ? GROUP BY d.doc_id, d.doc_rev, d.content "
540+ "ORDER BY d0.value;", ["key1", "a"],
541+ ["key1"], ["a"])
542+
543+ def test__format_query2(self):
544+ self.assertFormatQueryEquals(
545+ 'SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM '
546+ 'document d, document_fields d0, document_fields d1, '
547+ 'document_fields d2 LEFT OUTER JOIN conflicts c ON c.doc_id = '
548+ 'd.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name = ? AND '
549+ 'd0.value = ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND '
550+ 'd1.value = ? AND d.doc_id = d2.doc_id AND d2.field_name = ? AND '
551+ 'd2.value = ? GROUP BY d.doc_id, d.doc_rev, d.content ORDER BY '
552+ 'd0.value, d1.value, d2.value;',
553+ ["key1", "a", "key2", "b", "key3", "c"],
554+ ["key1", "key2", "key3"], ["a", "b", "c"])
555+
556+ def test__format_query_wildcard(self):
557+ self.assertFormatQueryEquals(
558+ 'SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM '
559+ 'document d, document_fields d0, document_fields d1, '
560+ 'document_fields d2 LEFT OUTER JOIN conflicts c ON c.doc_id = '
561+ 'd.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name = ? AND '
562+ 'd0.value = ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND '
563+ 'd1.value GLOB ? AND d.doc_id = d2.doc_id AND d2.field_name = ? '
564+ 'AND d2.value NOT NULL GROUP BY d.doc_id, d.doc_rev, d.content '
565+ 'ORDER BY d0.value, d1.value, d2.value;',
566+ ["key1", "a", "key2", "b*", "key3"], ["key1", "key2", "key3"],
567+ ["a", "b*", "*"])
568+
569+ def assertFormatRangeQueryEquals(self, exp_statement, exp_args, definition,
570+ start_value, end_value):
571+ statement, args = self.db._format_range_query(
572+ definition, start_value, end_value)
573+ self.assertEqual(exp_statement, statement)
574+ self.assertEqual(exp_args, args)
575+
576+ def test__format_range_query(self):
577+ self.assertFormatRangeQueryEquals(
578+ 'SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM '
579+ 'document d, document_fields d0, document_fields d1, '
580+ 'document_fields d2 LEFT OUTER JOIN conflicts c ON c.doc_id = '
581+ 'd.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name = ? AND '
582+ 'd0.value >= ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND '
583+ 'd1.value >= ? AND d.doc_id = d2.doc_id AND d2.field_name = ? AND '
584+ 'd2.value >= ? AND d.doc_id = d0.doc_id AND d0.field_name = ? AND '
585+ 'd0.value <= ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND '
586+ 'd1.value <= ? AND d.doc_id = d2.doc_id AND d2.field_name = ? AND '
587+ 'd2.value <= ? GROUP BY d.doc_id, d.doc_rev, d.content ORDER BY '
588+ 'd0.value, d1.value, d2.value;',
589+ ['key1', 'a', 'key2', 'b', 'key3', 'c', 'key1', 'p', 'key2', 'q',
590+ 'key3', 'r'],
591+ ["key1", "key2", "key3"], ["a", "b", "c"], ["p", "q", "r"])
592+
593+ def test__format_range_query_no_start(self):
594+ self.assertFormatRangeQueryEquals(
595+ 'SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM '
596+ 'document d, document_fields d0, document_fields d1, '
597+ 'document_fields d2 LEFT OUTER JOIN conflicts c ON c.doc_id = '
598+ 'd.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name = ? AND '
599+ 'd0.value <= ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND '
600+ 'd1.value <= ? AND d.doc_id = d2.doc_id AND d2.field_name = ? AND '
601+ 'd2.value <= ? GROUP BY d.doc_id, d.doc_rev, d.content ORDER BY '
602+ 'd0.value, d1.value, d2.value;',
603+ ['key1', 'a', 'key2', 'b', 'key3', 'c'],
604+ ["key1", "key2", "key3"], None, ["a", "b", "c"])
605+
606+ def test__format_range_query_no_end(self):
607+ self.assertFormatRangeQueryEquals(
608+ 'SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM '
609+ 'document d, document_fields d0, document_fields d1, '
610+ 'document_fields d2 LEFT OUTER JOIN conflicts c ON c.doc_id = '
611+ 'd.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name = ? AND '
612+ 'd0.value >= ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND '
613+ 'd1.value >= ? AND d.doc_id = d2.doc_id AND d2.field_name = ? AND '
614+ 'd2.value >= ? GROUP BY d.doc_id, d.doc_rev, d.content ORDER BY '
615+ 'd0.value, d1.value, d2.value;',
616+ ['key1', 'a', 'key2', 'b', 'key3', 'c'],
617+ ["key1", "key2", "key3"], ["a", "b", "c"], None)
618+
619+ def test__format_range_query_wildcard(self):
620+ self.assertFormatRangeQueryEquals(
621+ 'SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM '
622+ 'document d, document_fields d0, document_fields d1, '
623+ 'document_fields d2 LEFT OUTER JOIN conflicts c ON c.doc_id = '
624+ 'd.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name = ? AND '
625+ 'd0.value >= ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND '
626+ 'd1.value >= ? AND d.doc_id = d2.doc_id AND d2.field_name = ? AND '
627+ 'd2.value NOT NULL AND d.doc_id = d0.doc_id AND d0.field_name = ? '
628+ 'AND d0.value <= ? AND d.doc_id = d1.doc_id AND d1.field_name = ? '
629+ 'AND (d1.value < ? OR d1.value GLOB ?) AND d.doc_id = d2.doc_id '
630+ 'AND d2.field_name = ? AND d2.value NOT NULL GROUP BY d.doc_id, '
631+ 'd.doc_rev, d.content ORDER BY d0.value, d1.value, d2.value;',
632+ ['key1', 'a', 'key2', 'b', 'key3', 'key1', 'p', 'key2', 'q', 'q*',
633+ 'key3'],
634+ ["key1", "key2", "key3"], ["a", "b*", "*"], ["p", "q*", "*"])
635
636=== modified file 'u1db/tests/test_sync.py'
637--- u1db/tests/test_sync.py 2012-07-19 19:50:58 +0000
638+++ u1db/tests/test_sync.py 2012-08-10 20:11:19 +0000
639@@ -816,7 +816,10 @@
640 self.assertTransactionLog([doc_id, doc_id], self.db1)
641 self.assertGetDoc(self.db1, doc_id, doc2_rev, new_doc, True)
642 self.assertGetDoc(self.db2, doc_id, doc2_rev, new_doc, False)
643- self.assertEqual([doc2], self.db1.get_from_index('test-idx', 'altval'))
644+ from_idx = self.db1.get_from_index('test-idx', 'altval')[0]
645+ self.assertEqual(doc2.doc_id, from_idx.doc_id)
646+ self.assertEqual(doc2.rev, from_idx.rev)
647+ self.assertTrue(from_idx.has_conflicts)
648 self.assertEqual([], self.db1.get_from_index('test-idx', 'value'))
649
650 def test_sync_sees_remote_delete_conflicted(self):
651@@ -866,7 +869,10 @@
652 self.sync(self.db1, self.db2, trace_hook=after_whatschanged)
653 self.assertEqual([True], triggered)
654 self.assertGetDoc(self.db1, doc_id, doc2_rev2, content2, True)
655- self.assertEqual([doc], self.db1.get_from_index('test-idx', 'altval'))
656+ from_idx = self.db1.get_from_index('test-idx', 'altval')[0]
657+ self.assertEqual(doc.doc_id, from_idx.doc_id)
658+ self.assertEqual(doc.rev, from_idx.rev)
659+ self.assertTrue(from_idx.has_conflicts)
660 self.assertEqual([], self.db1.get_from_index('test-idx', 'value'))
661 self.assertEqual([], self.db1.get_from_index('test-idx', 'localval'))
662

Subscribers

People subscribed via source and target branches