Merge lp:~thisfred/u1db/validate_transaction_id_of_target into lp:u1db

Proposed by Eric Casteleijn
Status: Superseded
Proposed branch: lp:~thisfred/u1db/validate_transaction_id_of_target
Merge into: lp:u1db
Prerequisite: lp:~thisfred/u1db/txid_in_sync_exchange
Diff against target: 879 lines (+491/-37)
14 files modified
include/u1db/u1db.h (+4/-0)
include/u1db/u1db_internal.h (+31/-0)
src/u1db.c (+152/-16)
src/u1db_sync_target.c (+21/-13)
u1db/backends/__init__.py (+44/-0)
u1db/backends/inmemory.py (+12/-0)
u1db/backends/sqlite_backend.py (+22/-0)
u1db/errors.py (+8/-0)
u1db/sync.py (+8/-5)
u1db/tests/c_backend_wrapper.pyx (+50/-0)
u1db/tests/test_backends.py (+108/-1)
u1db/tests/test_c_backend.py (+8/-0)
u1db/tests/test_sqlite_backend.py (+5/-2)
u1db/tests/test_sync.py (+18/-0)
To merge this branch: bzr merge lp:~thisfred/u1db/validate_transaction_id_of_target
Reviewer Review Type Date Requested Status
Ubuntu One hackers Pending
Review via email: mp+110626@code.launchpad.net

This proposal supersedes a proposal from 2012-06-15.

This proposal has been superseded by a proposal from 2012-06-19.

Commit message

Added a validation function that checks that the generation + txid the sync target knows about us is correct.

Description of the change

Added a validation function that checks that the generation + txid the sync target knows about us is correct.

To post a comment you must log in.
338. By Eric Casteleijn

merged trunk resolved conflicts

339. By Eric Casteleijn

merged txid branch

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'include/u1db/u1db.h'
2--- include/u1db/u1db.h 2012-06-18 18:24:19 +0000
3+++ include/u1db/u1db.h 2012-06-18 18:24:19 +0000
4@@ -78,6 +78,10 @@
5 #define U1DB_CONVERGED 3
6 #define U1DB_CONFLICTED 4
7
8+// Used by validate_source_gen_and_trans_id
9+#define U1DB_INVALID_TRANSACTION_ID -20
10+#define U1DB_INVALID_GENERATION -21
11+
12 /**
13 * The basic constructor for a new connection.
14 */
15
16=== modified file 'include/u1db/u1db_internal.h'
17--- include/u1db/u1db_internal.h 2012-06-18 18:24:19 +0000
18+++ include/u1db/u1db_internal.h 2012-06-18 18:24:19 +0000
19@@ -22,6 +22,7 @@
20 #include <stdarg.h>
21 #include "u1db/u1db.h"
22 #include "u1db/compat.h"
23+#include "u1db/u1db_vectorclock.h"
24
25 typedef struct sqlite3 sqlite3;
26 typedef struct sqlite3_stmt sqlite3_stmt;
27@@ -208,11 +209,41 @@
28 int *state, int *at_gen);
29
30 /**
31+ * Validate source generation and transaction id.
32+ *
33+ * @param replica_uid uid of source replica at the time of the
34+ * change we are syncing.
35+ * @param replica_gen Generation of the replica at the time of the
36+ * change we are syncing.
37+ * @param replica_trans_id Transaction id of the replica at the time of the
38+ * change we are syncing.
39+ * @param cur_vcr Vector clock for the document in the database.
40+ * @param other_vcr Vector clock of the document being put.
41+ * @param state (OUT) 0 for success, U1DB_SUPERSEDED if the document is
42+ * superseded.
43+ */
44+int u1db__validate_source(u1database *db, const char *replica_uid,
45+ int replica_gen, const char *replica_trans_id,
46+ u1db_vectorclock *cur_vcr,
47+ u1db_vectorclock *other_vcr, int *state);
48+
49+/**
50 * Internal API, Get the global database rev.
51 */
52 int u1db__get_generation(u1database *db, int *generation);
53
54 /**
55+ * Internal API, Get the global database rev and transaction id.
56+ */
57+int u1db__get_generation_info(u1database *db, int *generation,
58+ char **trans_id);
59+
60+/**
61+ * Internal API, Validate generation and transaction id.
62+ */
63+int u1db_validate_gen_and_trans_id(u1database *db, int generation,
64+ const char *trans_id);
65+/**
66 * Internal API, Allocate a new document id, for cases when callers do not
67 * supply their own. Callers of this API are expected to free the result.
68 */
69
70=== modified file 'src/u1db.c'
71--- src/u1db.c 2012-06-12 12:35:05 +0000
72+++ src/u1db.c 2012-06-18 18:24:19 +0000
73@@ -722,6 +722,41 @@
74 return status;
75 }
76
77+int
78+u1db__validate_source(u1database *db, const char *replica_uid, int replica_gen,
79+ const char *replica_trans_id, u1db_vectorclock *cur,
80+ u1db_vectorclock *other, int *state)
81+{
82+ int old_generation;
83+ char *old_trans_id = NULL;
84+ int status = U1DB_OK;
85+
86+ *state = U1DB_OK;
87+ status = u1db__get_sync_gen_info(
88+ db, replica_uid, &old_generation, &old_trans_id);
89+ if (status != U1DB_OK)
90+ goto finish;
91+ if (replica_gen < old_generation) {
92+ if (u1db__vectorclock_is_newer(cur, other)) {
93+ *state = U1DB_SUPERSEDED;
94+ goto finish;
95+ }
96+ status = U1DB_INVALID_GENERATION;
97+ goto finish;
98+ }
99+ if (replica_gen > old_generation)
100+ goto finish;
101+ if (strcmp(replica_trans_id, old_trans_id) != 0) {
102+ status = U1DB_INVALID_TRANSACTION_ID;
103+ goto finish;
104+ }
105+ *state = U1DB_SUPERSEDED;
106+finish:
107+ if (old_trans_id != NULL)
108+ free(old_trans_id);
109+ return status;
110+}
111+
112
113 int
114 u1db__put_doc_if_newer(u1database *db, u1db_document *doc, int save_conflict,
115@@ -748,6 +783,31 @@
116 status = lookup_doc(db, doc->doc_id, &stored_doc_rev, &stored_content,
117 &stored_content_len, &statement);
118 if (status != SQLITE_OK) { goto finish; }
119+ // TODO: u1db__vectorclock_from_str returns NULL if there is an error
120+ // in the vector clock, or if we run out of memory... Probably
121+ // shouldn't be U1DB_NOMEM
122+ stored_vc = u1db__vectorclock_from_str(stored_doc_rev);
123+ if (stored_vc == NULL) {
124+ status = U1DB_NOMEM;
125+ goto finish;
126+ }
127+ new_vc = u1db__vectorclock_from_str(doc->doc_rev);
128+ if (new_vc == NULL) {
129+ status = U1DB_NOMEM;
130+ goto finish;
131+ }
132+ if (replica_uid != NULL && replica_trans_id != NULL) {
133+ status = u1db__validate_source(
134+ db, replica_uid, replica_gen, replica_trans_id, stored_vc, new_vc,
135+ state);
136+ if (status != U1DB_OK) {
137+ goto finish;
138+ }
139+ if (*state != U1DB_OK) {
140+ status = u1db__get_generation(db, at_gen);
141+ goto finish;
142+ }
143+ }
144 if (stored_doc_rev == NULL) {
145 status = U1DB_OK;
146 *state = U1DB_INSERTED;
147@@ -757,19 +817,6 @@
148 *state = U1DB_CONVERGED;
149 store = 0;
150 } else {
151- // TODO: u1db__vectorclock_from_str returns NULL if there is an error
152- // in the vector clock, or if we run out of memory... Probably
153- // shouldn't be U1DB_NOMEM
154- stored_vc = u1db__vectorclock_from_str(stored_doc_rev);
155- if (stored_vc == NULL) {
156- status = U1DB_NOMEM;
157- goto finish;
158- }
159- new_vc = u1db__vectorclock_from_str(doc->doc_rev);
160- if (new_vc == NULL) {
161- status = U1DB_NOMEM;
162- goto finish;
163- }
164 if (u1db__vectorclock_is_newer(new_vc, stored_vc)) {
165 // Just take the newer version
166 store = 1;
167@@ -818,8 +865,8 @@
168 (stored_doc_rev != NULL));
169 }
170 if (status == U1DB_OK && replica_uid != NULL) {
171- status = u1db__set_sync_info(db, replica_uid, replica_gen,
172- replica_trans_id);
173+ status = u1db__set_sync_info(
174+ db, replica_uid, replica_gen, replica_trans_id);
175 }
176 if (status == U1DB_OK && at_gen != NULL) {
177 status = u1db__get_generation(db, at_gen);
178@@ -1182,14 +1229,26 @@
179 status = U1DB_OK;
180 *gen = 0;
181 *trans_id = strdup("");
182+ if (*trans_id == NULL) {
183+ status = U1DB_NOMEM;
184+ goto finish;
185+ }
186 } else if (status == SQLITE_ROW) {
187 status = U1DB_OK;
188 *gen = sqlite3_column_int(statement, 0);
189 tmp = (const char *)sqlite3_column_text(statement, 1);
190 if (tmp == NULL) {
191 *trans_id = strdup("");
192+ if (*trans_id == NULL) {
193+ status = U1DB_NOMEM;
194+ goto finish;
195+ }
196 } else {
197 *trans_id = strdup(tmp);
198+ if (*trans_id == NULL) {
199+ status = U1DB_NOMEM;
200+ goto finish;
201+ }
202 }
203 }
204 finish:
205@@ -1329,6 +1388,80 @@
206 return status;
207 }
208
209+int
210+u1db__get_generation_info(u1database *db, int *generation, char **trans_id)
211+{
212+ int status;
213+ const char *tmp;
214+
215+ sqlite3_stmt *statement;
216+ if (db == NULL || generation == NULL) {
217+ return U1DB_INVALID_PARAMETER;
218+ }
219+ status = sqlite3_prepare_v2(db->sql_handle,
220+ "SELECT max(generation), transaction_id FROM transaction_log", -1,
221+ &statement, NULL);
222+ if (status != SQLITE_OK) {
223+ return status;
224+ }
225+ status = sqlite3_step(statement);
226+ if (status == SQLITE_DONE) {
227+ // No records, we are at rev 0
228+ status = SQLITE_OK;
229+ *generation = 0;
230+ } else if (status == SQLITE_ROW) {
231+ status = SQLITE_OK;
232+ *generation = sqlite3_column_int(statement, 0);
233+ tmp = (const char *)sqlite3_column_text(statement, 1);
234+ if (tmp == NULL) {
235+ *trans_id = NULL;
236+ } else {
237+ *trans_id = strdup(tmp);
238+ if (*trans_id == NULL) {
239+ status = U1DB_NOMEM;
240+ }
241+ }
242+ }
243+ sqlite3_finalize(statement);
244+ return status;
245+}
246+
247+int
248+u1db_validate_gen_and_trans_id(u1database *db, int generation,
249+ const char *trans_id)
250+{
251+ int status = U1DB_OK;
252+ sqlite3_stmt *statement;
253+
254+ if (generation == 0)
255+ return status;
256+ if (db == NULL) {
257+ return U1DB_INVALID_PARAMETER;
258+ }
259+ status = sqlite3_prepare_v2(db->sql_handle,
260+ "SELECT transaction_id FROM transaction_log WHERE generation = ?", -1,
261+ &statement, NULL);
262+ if (status != SQLITE_OK) { goto finish; }
263+ status = sqlite3_bind_int(statement, 1, generation);
264+ if (status != SQLITE_OK) { goto finish; }
265+ status = sqlite3_step(statement);
266+ if (status == SQLITE_DONE) {
267+ status = U1DB_INVALID_GENERATION;
268+ goto finish;
269+ } else if (status == SQLITE_ROW) {
270+ // Note: We may want to handle the column containing NULL
271+ if (strcmp(trans_id,
272+ (const char *)sqlite3_column_text(statement, 0)) == 0) {
273+ status = U1DB_OK;
274+ goto finish;
275+ }
276+ status = U1DB_INVALID_TRANSACTION_ID;
277+ }
278+finish:
279+ sqlite3_finalize(statement);
280+ return status;
281+}
282+
283 char *
284 u1db__allocate_doc_id(u1database *db)
285 {
286@@ -1462,7 +1595,10 @@
287 // Note: We may want to handle the column containing NULL
288 tmp = (const char *)sqlite3_column_text(statement, 1);
289 if (tmp == NULL) {
290- *trans_id = NULL;
291+ *trans_id = strdup("");
292+ if (*trans_id == NULL) {
293+ status = U1DB_NOMEM;
294+ }
295 } else {
296 *trans_id = strdup(tmp);
297 if (*trans_id == NULL) {
298
299=== modified file 'src/u1db_sync_target.c'
300--- src/u1db_sync_target.c 2012-06-18 18:24:19 +0000
301+++ src/u1db_sync_target.c 2012-06-18 18:24:19 +0000
302@@ -24,7 +24,7 @@
303 static int st_get_sync_info(u1db_sync_target *st,
304 const char *source_replica_uid,
305 const char **st_replica_uid, int *st_gen, int *source_gen,
306- char **trans_id);
307+ char **source_trans_id);
308
309 static int st_record_sync_info(u1db_sync_target *st,
310 const char *source_replica_uid, int source_gen, const char *trans_id);
311@@ -106,7 +106,7 @@
312 static int
313 st_get_sync_info(u1db_sync_target *st, const char *source_replica_uid,
314 const char **st_replica_uid, int *st_gen, int *source_gen,
315- char **trans_id)
316+ char **source_trans_id)
317 {
318 int status = U1DB_OK;
319 u1database *db;
320@@ -125,8 +125,8 @@
321 db = (u1database *)st->implementation;
322 status = u1db_get_replica_uid(db, st_replica_uid);
323 if (status != U1DB_OK) { goto finish; }
324- status = u1db__get_sync_gen_info(db, source_replica_uid, source_gen,
325- trans_id);
326+ status = u1db__get_sync_gen_info(
327+ db, source_replica_uid, source_gen, source_trans_id);
328 if (status != U1DB_OK) { goto finish; }
329 status = u1db__get_generation(db, st_gen);
330 finish:
331@@ -614,6 +614,9 @@
332 status = target->get_sync_info(target, local_uid, &target_uid, &target_gen,
333 &local_gen_known_by_target, &local_trans_id_known_by_target);
334 if (status != U1DB_OK) { goto finish; }
335+ status = u1db_validate_gen_and_trans_id(
336+ db, local_gen_known_by_target, local_trans_id_known_by_target);
337+ if (status != U1DB_OK) { goto finish; }
338 status = u1db__get_sync_gen_info(db, target_uid,
339 &target_gen_known_by_local, &target_trans_id_known_by_local);
340 if (status != U1DB_OK) { goto finish; }
341@@ -623,8 +626,9 @@
342 // Before we start the sync exchange, get the list of doc_ids that we want
343 // to send. We have to do this first, so that local_gen_before_sync will
344 // match exactly the list of doc_ids we send
345- status = u1db_whats_changed(db, &local_gen, &local_trans_id,
346- (void*)&to_send_state, whats_changed_to_doc_ids);
347+ status = u1db_whats_changed(
348+ db, &local_gen, &local_trans_id, (void*)&to_send_state,
349+ whats_changed_to_doc_ids);
350 if (status != U1DB_OK) { goto finish; }
351 if (local_gen == local_gen_known_by_target
352 && target_gen == target_gen_known_by_local)
353@@ -644,19 +648,23 @@
354 &target_gen_known_by_local, &target_trans_id_known_by_local,
355 &return_doc_state, return_doc_to_insert_from_target);
356 if (status != U1DB_OK) { goto finish; }
357- status = u1db__get_generation(db, &local_gen);
358+ if (local_trans_id != NULL) {
359+ free(local_trans_id);
360+ }
361+ status = u1db__get_generation_info(db, &local_gen, &local_trans_id);
362 if (status != U1DB_OK) { goto finish; }
363 // Now we successfully sent and received docs, make sure we record the
364 // current remote generation
365- status = u1db__set_sync_info(db, target_uid, target_gen_known_by_local,
366- "T-sid");
367+ status = u1db__set_sync_info(
368+ db, target_uid, target_gen_known_by_local,
369+ target_trans_id_known_by_local);
370 if (status != U1DB_OK) { goto finish; }
371 if (return_doc_state.num_inserted > 0 &&
372- ((*local_gen_before_sync + return_doc_state.num_inserted)
373- == local_gen))
374+ ((*local_gen_before_sync + return_doc_state.num_inserted)
375+ == local_gen))
376 {
377- status = target->record_sync_info(target, local_uid, local_gen,
378- "T-sid");
379+ status = target->record_sync_info(
380+ target, local_uid, local_gen, local_trans_id);
381 if (status != U1DB_OK) { goto finish; }
382 }
383 finish:
384
385=== modified file 'u1db/backends/__init__.py'
386--- u1db/backends/__init__.py 2012-05-30 14:39:36 +0000
387+++ u1db/backends/__init__.py 2012-06-18 18:24:19 +0000
388@@ -53,8 +53,17 @@
389 raise errors.InvalidDocId()
390
391 def _get_generation(self):
392+ """Return the current generation.
393+
394+ """
395 raise NotImplementedError(self._get_generation)
396
397+ def _get_generation_info(self):
398+ """Return the current generation and transaction id.
399+
400+ """
401+ raise NotImplementedError(self._get_generation_info)
402+
403 def _get_doc(self, doc_id):
404 """Extract the document from storage.
405
406@@ -93,6 +102,35 @@
407 result.append(doc)
408 return result
409
410+ def validate_gen_and_trans_id(self, generation, trans_id):
411+ """Validate the generation and transaction id.
412+
413+ Raises an InvalidGeneration when the generation does not exist, and an
414+ InvalidTransactionId when it does but with a different transaction id.
415+
416+ """
417+ raise NotImplementedError(self.validate_gen_and_trans_id)
418+
419+ def _validate_source(self, other_replica_uid, other_generation,
420+ other_transaction_id, cur_vcr, other_vcr):
421+ """Validate the new generation and transaction id.
422+
423+ other_generation must be greater than what we have stored for this
424+ replica, *or* it must be the same and the transaction_id must be the
425+ same as well.
426+ """
427+ old_generation, old_transaction_id = self._get_sync_gen_info(
428+ other_replica_uid)
429+ if other_generation < old_generation:
430+ if cur_vcr.is_newer(other_vcr):
431+ return 'superseded'
432+ raise errors.InvalidGeneration
433+ if other_generation > old_generation:
434+ return 'ok'
435+ if other_transaction_id == old_transaction_id:
436+ return 'superseded'
437+ raise errors.InvalidTransactionId
438+
439 def _put_doc_if_newer(self, doc, save_conflict, replica_uid=None,
440 replica_gen=None, replica_trans_id=None):
441 cur_doc = self._get_doc(doc.doc_id)
442@@ -101,6 +139,12 @@
443 cur_vcr = VectorClockRev(None)
444 else:
445 cur_vcr = VectorClockRev(cur_doc.rev)
446+ if replica_uid is not None and replica_gen is not None:
447+ state = self._validate_source(
448+ replica_uid, replica_gen, replica_trans_id, cur_vcr,
449+ doc_vcr)
450+ if state != 'ok':
451+ return state, self._get_generation()
452 if doc_vcr.is_newer(cur_vcr):
453 self._put_and_update_indexes(cur_doc, doc)
454 self._prune_conflicts(doc, doc_vcr)
455
456=== modified file 'u1db/backends/inmemory.py'
457--- u1db/backends/inmemory.py 2012-06-12 15:44:43 +0000
458+++ u1db/backends/inmemory.py 2012-06-18 18:24:19 +0000
459@@ -79,6 +79,18 @@
460 def _get_generation(self):
461 return len(self._transaction_log)
462
463+ def _get_generation_info(self):
464+ return len(self._transaction_log), self._transaction_log[-1][1]
465+
466+ def validate_gen_and_trans_id(self, generation, trans_id):
467+ if generation == 0:
468+ return
469+ if generation > len(self._transaction_log):
470+ raise errors.InvalidGeneration
471+ if self._transaction_log[generation - 1][1] == trans_id:
472+ return
473+ raise errors.InvalidTransactionId
474+
475 def put_doc(self, doc):
476 if doc.doc_id is None:
477 raise errors.InvalidDocId()
478
479=== modified file 'u1db/backends/sqlite_backend.py'
480--- u1db/backends/sqlite_backend.py 2012-06-12 15:44:43 +0000
481+++ u1db/backends/sqlite_backend.py 2012-06-18 18:24:19 +0000
482@@ -265,6 +265,28 @@
483 return 0
484 return val
485
486+ def _get_generation_info(self):
487+ c = self._db_handle.cursor()
488+ c.execute(
489+ 'SELECT max(generation), transaction_id FROM transaction_log ')
490+ val = c.fetchone()
491+ if val[0] is None:
492+ return(0, val[1])
493+ return val
494+
495+ def validate_gen_and_trans_id(self, generation, trans_id):
496+ if generation == 0:
497+ return
498+ c = self._db_handle.cursor()
499+ c.execute(
500+ 'SELECT transaction_id FROM transaction_log WHERE generation = ?',
501+ (generation,))
502+ val = c.fetchone()
503+ if val is None:
504+ raise errors.InvalidGeneration
505+ if val[0] != trans_id:
506+ raise errors.InvalidTransactionId
507+
508 def _get_transaction_log(self):
509 c = self._db_handle.cursor()
510 c.execute("SELECT doc_id, transaction_id FROM transaction_log"
511
512=== modified file 'u1db/errors.py'
513--- u1db/errors.py 2012-05-25 18:14:06 +0000
514+++ u1db/errors.py 2012-06-18 18:24:19 +0000
515@@ -39,6 +39,14 @@
516 wire_description = "invalid document id"
517
518
519+class InvalidTransactionId(U1DBError):
520+ """Invalid transaction for generation."""
521+
522+
523+class InvalidGeneration(U1DBError):
524+ """Generation was previously synced with a different transaction id."""
525+
526+
527 class ConflictedDoc(U1DBError):
528 """The document is conflicted, you must call resolve before put()"""
529
530
531=== modified file 'u1db/sync.py'
532--- u1db/sync.py 2012-06-18 18:24:19 +0000
533+++ u1db/sync.py 2012-06-18 18:24:19 +0000
534@@ -83,11 +83,11 @@
535 record with the target that they are fully up to date with our
536 new generation.
537 """
538- cur_gen = self.source._get_generation()
539+ cur_gen, trans_id = self.source._get_generation_info()
540 if (cur_gen == start_generation + self.num_inserted
541- and self.num_inserted > 0):
542- self.sync_target.record_sync_info(self.source._replica_uid,
543- cur_gen, 'T-sid')
544+ and self.num_inserted > 0):
545+ self.sync_target.record_sync_info(
546+ self.source._replica_uid, cur_gen, trans_id)
547
548 def sync(self, callback=None):
549 """Synchronize documents between source and target."""
550@@ -98,6 +98,8 @@
551 target_my_trans_id) = sync_target.get_sync_info(
552 self.source._replica_uid)
553 # what's changed since that generation and this current gen
554+ self.source.validate_gen_and_trans_id(
555+ target_my_gen, target_my_trans_id)
556 my_gen, _, changes = self.source.whats_changed(target_my_gen)
557
558 # this source last-seen database generation for the target
559@@ -121,7 +123,8 @@
560 self.source._replica_uid, target_last_known_gen,
561 return_doc_cb=self._insert_doc_from_target)
562 # record target synced-up-to generation including applying what we sent
563- self.source._set_sync_info(self.target_replica_uid, new_gen, 'T-sid')
564+ self.source._set_sync_info(
565+ self.target_replica_uid, new_gen, new_trans_id)
566
567 # if gapless record current reached generation with target
568 self._record_sync_info_with_the_target(my_gen)
569
570=== modified file 'u1db/tests/c_backend_wrapper.pyx'
571--- u1db/tests/c_backend_wrapper.pyx 2012-06-18 18:24:19 +0000
572+++ u1db/tests/c_backend_wrapper.pyx 2012-06-18 18:24:19 +0000
573@@ -80,6 +80,10 @@
574 int u1db_get_all_docs(u1database *db, int include_deleted, int *generation,
575 void *context, u1db_doc_callback cb)
576 int u1db_put_doc(u1database *db, u1db_document *doc)
577+ int u1db__validate_source(u1database *db, const_char_ptr replica_uid,
578+ int replica_gen, const_char_ptr replica_trans_id,
579+ u1db_vectorclock *cur_vcr,
580+ u1db_vectorclock *other_vcr, int *state)
581 int u1db__put_doc_if_newer(u1database *db, u1db_document *doc,
582 int save_conflict, char *replica_uid,
583 int replica_gen, char *replica_trans_id,
584@@ -131,6 +135,8 @@
585 int U1DB_BROKEN_SYNC_STREAM
586 int U1DB_DUPLICATE_INDEX_NAME
587 int U1DB_INDEX_DOES_NOT_EXIST
588+ int U1DB_INVALID_GENERATION
589+ int U1DB_INVALID_TRANSACTION_ID
590 int U1DB_INTERNAL_ERROR
591
592 int U1DB_INSERTED
593@@ -198,6 +204,9 @@
594
595
596 int u1db__get_generation(u1database *, int *db_rev)
597+ int u1db__get_generation_info(u1database *, int *db_rev, char **trans_id)
598+ int u1db_validate_gen_and_trans_id(u1database *, int db_rev,
599+ const_char_ptr trans_id)
600 char *u1db__allocate_doc_id(u1database *)
601 int u1db__sql_close(u1database *)
602 int u1db__sql_is_open(u1database *)
603@@ -580,6 +589,10 @@
604 raise errors.IndexNameTakenError()
605 if status == U1DB_INDEX_DOES_NOT_EXIST:
606 raise errors.IndexDoesNotExist
607+ if status == U1DB_INVALID_GENERATION:
608+ raise errors.InvalidGeneration
609+ if status == U1DB_INVALID_TRANSACTION_ID:
610+ raise errors.InvalidTransactionId
611 raise RuntimeError('%s (status: %s)' % (context, status))
612
613
614@@ -912,6 +925,27 @@
615 u1db_put_doc(self._db, doc._doc))
616 return doc.rev
617
618+ def _validate_source(self, replica_uid, replica_gen, replica_trans_id,
619+ cur_vcr, other_vcr):
620+ cdef const_char_ptr c_uid, c_trans_id
621+ cdef int c_gen, state = 0
622+ cdef VectorClockRev cur
623+ cdef VectorClockRev other
624+
625+ cur = VectorClockRev(cur_vcr.as_str())
626+ other = VectorClockRev(other_vcr.as_str())
627+ c_uid = replica_uid
628+ c_trans_id = replica_trans_id
629+ c_gen = replica_gen
630+ handle_status(
631+ "invalid generation or transaction id",
632+ u1db__validate_source(
633+ self._db, c_uid, c_gen, c_trans_id, cur._clock, other._clock,
634+ &state))
635+ if state == U1DB_SUPERSEDED:
636+ return 'superseded'
637+ return 'ok'
638+
639 def _put_doc_if_newer(self, CDocument doc, save_conflict, replica_uid=None,
640 replica_gen=None, replica_trans_id=None):
641 cdef char *c_uid, *c_trans_id
642@@ -1033,6 +1067,22 @@
643 u1db__get_generation(self._db, &generation))
644 return generation
645
646+ def _get_generation_info(self):
647+ cdef int generation
648+ cdef char *trans_id
649+ handle_status("get_generation_info",
650+ u1db__get_generation_info(self._db, &generation, &trans_id))
651+ raw_trans_id = None
652+ if trans_id != NULL:
653+ raw_trans_id = trans_id
654+ free(trans_id)
655+ return generation, raw_trans_id
656+
657+ def validate_gen_and_trans_id(self, generation, trans_id):
658+ handle_status(
659+ "validate_gen_and_trans_id",
660+ u1db_validate_gen_and_trans_id(self._db, generation, trans_id))
661+
662 def _get_sync_gen_info(self, replica_uid):
663 cdef int generation, status
664 cdef char *trans_id = NULL
665
666=== modified file 'u1db/tests/test_backends.py'
667--- u1db/tests/test_backends.py 2012-06-18 18:24:19 +0000
668+++ u1db/tests/test_backends.py 2012-06-18 18:24:19 +0000
669@@ -392,6 +392,112 @@
670 # The database wasn't altered
671 self.assertGetDoc(self.db, doc1.doc_id, doc1.rev, simple_doc, False)
672
673+ def test_put_doc_if_newer_newer_generation(self):
674+ self.db._set_sync_info('other', 1, 'T-sid')
675+ doc = self.make_document('doc_id', 'other:2', simple_doc)
676+ state, _ = self.db._put_doc_if_newer(
677+ doc, save_conflict=False, replica_uid='other', replica_gen=2,
678+ replica_trans_id='T-irrelevant')
679+ self.assertEqual('inserted', state)
680+
681+ def test_put_doc_if_newer_same_generation_same_txid(self):
682+ self.db._set_sync_info('other', 1, 'T-sid')
683+ doc = self.make_document('doc_id', 'other:2', simple_doc)
684+ state, _ = self.db._put_doc_if_newer(
685+ doc, save_conflict=False, replica_uid='other', replica_gen=1,
686+ replica_trans_id='T-sid')
687+ self.assertEqual('superseded', state)
688+
689+ def test_put_doc_if_newer_wrong_transaction_id(self):
690+ self.db._set_sync_info('other', 1, 'T-sid')
691+ doc = self.make_document('doc_id', 'other:1', simple_doc)
692+ self.assertRaises(
693+ errors.InvalidTransactionId,
694+ self.db._put_doc_if_newer, doc, save_conflict=False,
695+ replica_uid='other', replica_gen=1, replica_trans_id='T-sad')
696+
697+ def test_put_doc_if_newer_old_generation_older_doc(self):
698+ orig_doc = '{"new": "doc"}'
699+ doc = self.db.create_doc(orig_doc)
700+ doc_rev1 = doc.rev
701+ doc.set_json(simple_doc)
702+ self.db.put_doc(doc)
703+ self.db._set_sync_info('other', 5, 'T-sid')
704+ older_doc = self.make_document(doc.doc_id, doc_rev1, simple_doc)
705+ state, _ = self.db._put_doc_if_newer(
706+ older_doc, save_conflict=False, replica_uid='other', replica_gen=3,
707+ replica_trans_id='T-irrelevant')
708+ self.assertEqual('superseded', state)
709+
710+ def test_put_doc_if_newer_old_generation_newer_doc(self):
711+ self.db._set_sync_info('other', 5, 'T-sid')
712+ doc = self.make_document('doc_id', 'other:1', simple_doc)
713+ self.assertRaises(
714+ errors.InvalidGeneration,
715+ self.db._put_doc_if_newer, doc, save_conflict=False,
716+ replica_uid='other', replica_gen=1, replica_trans_id='T-sad')
717+
718+ def test_validate_gen_and_trans_id(self):
719+ self.db.create_doc(simple_doc)
720+ gen, trans_id = self.db._get_generation_info()
721+ self.db.validate_gen_and_trans_id(gen, trans_id)
722+
723+ def test_validate_gen_and_trans_id_invalid_txid(self):
724+ self.db.create_doc(simple_doc)
725+ gen, _ = self.db._get_generation_info()
726+ self.assertRaises(
727+ errors.InvalidTransactionId,
728+ self.db.validate_gen_and_trans_id, gen, 'wrong')
729+
730+ def test_validate_gen_and_trans_id_invalid_txid(self):
731+ self.db.create_doc(simple_doc)
732+ gen, trans_id = self.db._get_generation_info()
733+ self.assertRaises(
734+ errors.InvalidGeneration,
735+ self.db.validate_gen_and_trans_id, gen + 1, trans_id)
736+
737+ def test_validate_source_gen_and_trans_id_same(self):
738+ self.db._set_sync_info('other', 1, 'T-sid')
739+ v1 = vectorclock.VectorClockRev('other:1|self:1')
740+ v2 = vectorclock.VectorClockRev('other:2|self:2')
741+ self.assertEqual(
742+ 'superseded',
743+ self.db._validate_source('other', 1, 'T-sid', v1, v2))
744+
745+ def test_validate_source_gen_newer(self):
746+ self.db._set_sync_info('other', 1, 'T-sid')
747+ v1 = vectorclock.VectorClockRev('other:1|self:1')
748+ v2 = vectorclock.VectorClockRev('other:2|self:2')
749+ self.assertEqual(
750+ 'ok',
751+ self.db._validate_source('other', 2, 'T-whatevs', v1, v2))
752+
753+ def test_validate_source_wrong_txid(self):
754+ self.db._set_sync_info('other', 1, 'T-sid')
755+ v1 = vectorclock.VectorClockRev('other:1|self:1')
756+ v2 = vectorclock.VectorClockRev('other:2|self:2')
757+ self.assertRaises(
758+ errors.InvalidTransactionId,
759+ self.db._validate_source, 'other', 1, 'T-sad', v1, v2)
760+
761+ def test_validate_source_gen_older_and_vcr_older(self):
762+ self.db._set_sync_info('other', 1, 'T-sid')
763+ self.db._set_sync_info('other', 2, 'T-sod')
764+ v1 = vectorclock.VectorClockRev('other:1|self:1')
765+ v2 = vectorclock.VectorClockRev('other:2|self:2')
766+ self.assertEqual(
767+ 'superseded',
768+ self.db._validate_source('other', 1, 'T-sid', v2, v1))
769+
770+ def test_validate_source_gen_older_vcr_newer(self):
771+ self.db._set_sync_info('other', 1, 'T-sid')
772+ self.db._set_sync_info('other', 2, 'T-sod')
773+ v1 = vectorclock.VectorClockRev('other:1|self:1')
774+ v2 = vectorclock.VectorClockRev('other:2|self:2')
775+ self.assertRaises(
776+ errors.InvalidGeneration,
777+ self.db._validate_source, 'other', 1, 'T-sid', v1, v2)
778+
779 def test_put_doc_if_newer_replica_uid(self):
780 doc1 = self.db.create_doc(simple_doc)
781 self.db._set_sync_info('other', 1, 'T-sid')
782@@ -636,7 +742,8 @@
783 doc2 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc)
784 self.db._put_doc_if_newer(doc2, save_conflict=True)
785 self.assertTrue(doc2.has_conflicts)
786- self.assertGetDoc(self.db, doc1.doc_id, 'alternate:1', nested_doc, True)
787+ self.assertGetDoc(
788+ self.db, doc1.doc_id, 'alternate:1', nested_doc, True)
789 self.assertGetDocConflicts(self.db, doc1.doc_id,
790 [('alternate:1', nested_doc), (doc1.rev, None)])
791
792
793=== modified file 'u1db/tests/test_c_backend.py'
794--- u1db/tests/test_c_backend.py 2012-06-18 18:24:19 +0000
795+++ u1db/tests/test_c_backend.py 2012-06-18 18:24:19 +0000
796@@ -73,6 +73,14 @@
797 db.create_doc(tests.simple_doc)
798 self.assertEqual(1, db._get_generation())
799
800+ def test__get_generation_info(self):
801+ db = c_backend_wrapper.CDatabase(':memory:')
802+ self.assertEqual((0, None), db._get_generation_info())
803+ db.create_doc(tests.simple_doc)
804+ info = db._get_generation_info()
805+ self.assertEqual(1, info[0])
806+ self.assertTrue(info[1].startswith('T-'))
807+
808 def test__set_replica_uid(self):
809 db = c_backend_wrapper.CDatabase(':memory:')
810 self.assertIsNot(None, db._replica_uid)
811
812=== modified file 'u1db/tests/test_sqlite_backend.py'
813--- u1db/tests/test_sqlite_backend.py 2012-05-31 16:31:31 +0000
814+++ u1db/tests/test_sqlite_backend.py 2012-06-18 18:24:19 +0000
815@@ -152,6 +152,9 @@
816 def test__get_generation(self):
817 self.assertEqual(0, self.db._get_generation())
818
819+ def test__get_generation_info(self):
820+ self.assertEqual((0, None), self.db._get_generation_info())
821+
822 def test_create_index(self):
823 self.db.create_index('test-idx', "key")
824 self.assertEqual([('test-idx', ["key"])], self.db.list_indexes())
825@@ -261,7 +264,7 @@
826 def test__open_database_with_factory(self):
827 temp_dir = self.createTempDir(prefix='u1db-test-')
828 path = temp_dir + '/test.sqlite'
829- db = sqlite_backend.SQLitePartialExpandDatabase(path)
830+ sqlite_backend.SQLitePartialExpandDatabase(path)
831 db2 = sqlite_backend.SQLiteDatabase._open_database(
832 path, document_factory=TestAlternativeDocument)
833 self.assertEqual(TestAlternativeDocument, db2._factory)
834@@ -322,7 +325,7 @@
835 def test_open_database_with_factory(self):
836 temp_dir = self.createTempDir(prefix='u1db-test-')
837 path = temp_dir + '/existing.sqlite'
838- db = sqlite_backend.SQLitePartialExpandDatabase(path)
839+ sqlite_backend.SQLitePartialExpandDatabase(path)
840 db2 = sqlite_backend.SQLiteDatabase.open_database(
841 path, create=False, document_factory=TestAlternativeDocument)
842 self.assertEqual(TestAlternativeDocument, db2._factory)
843
844=== modified file 'u1db/tests/test_sync.py'
845--- u1db/tests/test_sync.py 2012-06-18 18:24:19 +0000
846+++ u1db/tests/test_sync.py 2012-06-18 18:24:19 +0000
847@@ -662,6 +662,12 @@
848 doc1 = self.db1.create_doc('{"a": 1}', doc_id='the-doc')
849 db3 = self.create_database('test3')
850 self.sync(self.db2, self.db1)
851+ self.assertEqual(
852+ self.db1._get_generation_info(),
853+ self.db2._get_sync_gen_info(self.db1._replica_uid))
854+ self.assertEqual(
855+ self.db2._get_generation_info(),
856+ self.db1._get_sync_gen_info(self.db2._replica_uid))
857 self.sync(db3, self.db1)
858 # update on 2
859 doc2 = self.make_document('the-doc', doc1.rev, '{"a": 2}')
860@@ -695,7 +701,19 @@
861 self.db2.create_doc('{"b": 1}', doc_id='the-doc')
862 db3.create_doc('{"c": 1}', doc_id='the-doc')
863 self.sync(db3, self.db1)
864+ self.assertEqual(
865+ self.db1._get_generation_info(),
866+ db3._get_sync_gen_info(self.db1._replica_uid))
867+ self.assertEqual(
868+ db3._get_generation_info(),
869+ self.db1._get_sync_gen_info(db3._replica_uid))
870 self.sync(db3, self.db2)
871+ self.assertEqual(
872+ self.db2._get_generation_info(),
873+ db3._get_sync_gen_info(self.db2._replica_uid))
874+ self.assertEqual(
875+ db3._get_generation_info(),
876+ self.db2._get_sync_gen_info(db3._replica_uid))
877 self.assertEqual(3, len(db3.get_doc_conflicts('the-doc')))
878 doc1.set_json('{"a": 2}')
879 self.db1.put_doc(doc1)

Subscribers

People subscribed via source and target branches