Permalink
Browse files

Use SQLite savepoints to support nested transactions

TDDatabase can now abort/rollback a transaction even if it's nested inside another one.
Also simplified the transaction API a bit.
  • Loading branch information...
1 parent f77426d commit ccb04b6f339b18761c813568e2018233a1215ae6 @snej snej committed Dec 29, 2011
Showing with 115 additions and 126 deletions.
  1. +87 −81 Source/TDDatabase+Insertion.m
  2. +7 −5 Source/TDDatabase.h
  3. +17 −33 Source/TDDatabase.m
  4. +0 −1 Source/TDDatabase_Tests.m
  5. +1 −1 Source/TDPuller.m
  6. +3 −5 Source/TDView.m
@@ -260,9 +260,7 @@ - (TDRevision*) putRevision: (TDRevision*)rev
} @finally {
// Remember, we could have gotten here via a 'return' inside the @try block above.
[r close];
- if (*outStatus >= 300)
- self.transactionFailed = YES;
- [self endTransaction];
+ [self endTransaction: (*outStatus < 300)];
}
if (*outStatus >= 300)
@@ -278,92 +276,100 @@ - (TDStatus) forceInsert: (TDRevision*)rev
revisionHistory: (NSArray*)history // in *reverse* order, starting with rev's revID
source: (NSURL*)source
{
- // First look up all locally-known revisions of this document:
- NSString* docID = rev.docID;
- SInt64 docNumericID = [self getOrInsertDocNumericID: docID];
- TDRevisionList* localRevs = [self getAllRevisionsOfDocumentID: docID
- numericID: docNumericID
- onlyCurrent: NO];
- if (!localRevs)
- return 500;
- NSUInteger historyCount = history.count;
- Assert(historyCount >= 1);
-
- // Validate against the latest common ancestor:
- if (_validations.count > 0) {
- TDRevision* oldRev = nil;
- for (NSUInteger i = 1; i<historyCount; ++i) {
- oldRev = [localRevs revWithDocID: docID revID: [history objectAtIndex: i]];
- if (oldRev)
- break;
+ BOOL success = NO;
+ [self beginTransaction];
+ @try {
+ // First look up all locally-known revisions of this document:
+ NSString* docID = rev.docID;
+ SInt64 docNumericID = [self getOrInsertDocNumericID: docID];
+ TDRevisionList* localRevs = [self getAllRevisionsOfDocumentID: docID
+ numericID: docNumericID
+ onlyCurrent: NO];
+ if (!localRevs)
+ return 500;
+ NSUInteger historyCount = history.count;
+ Assert(historyCount >= 1);
+
+ // Validate against the latest common ancestor:
+ if (_validations.count > 0) {
+ TDRevision* oldRev = nil;
+ for (NSUInteger i = 1; i<historyCount; ++i) {
+ oldRev = [localRevs revWithDocID: docID revID: [history objectAtIndex: i]];
+ if (oldRev)
+ break;
+ }
+ TDStatus status = [self validateRevision: rev previousRevision: oldRev];
+ if (status >= 300)
+ return status;
}
- TDStatus status = [self validateRevision: rev previousRevision: oldRev];
- if (status >= 300)
- return status;
- }
-
- // Walk through the remote history in chronological order, matching each revision ID to
- // a local revision. When the list diverges, start creating blank local revisions to fill
- // in the local history:
- SequenceNumber sequence = 0;
- SequenceNumber localParentSequence = 0;
- for (NSInteger i = historyCount - 1; i>=0; --i) {
- NSString* revID = [history objectAtIndex: i];
- TDRevision* localRev = [localRevs revWithDocID: docID revID: revID];
- if (localRev) {
- // This revision is known locally. Remember its sequence as the parent of the next one:
- sequence = localRev.sequence;
- Assert(sequence > 0);
- localParentSequence = sequence;
-
- } else {
- // This revision isn't known, so add it:
- TDRevision* newRev;
- NSData* json = nil;
- BOOL current = NO;
- if (i==0) {
- // Hey, this is the leaf revision we're inserting:
- newRev = rev;
- if (!rev.deleted) {
- json = [self encodeDocumentJSON: rev];
- if (!json)
- return 400;
- }
- current = YES;
+
+ // Walk through the remote history in chronological order, matching each revision ID to
+ // a local revision. When the list diverges, start creating blank local revisions to fill
+ // in the local history:
+ SequenceNumber sequence = 0;
+ SequenceNumber localParentSequence = 0;
+ for (NSInteger i = historyCount - 1; i>=0; --i) {
+ NSString* revID = [history objectAtIndex: i];
+ TDRevision* localRev = [localRevs revWithDocID: docID revID: revID];
+ if (localRev) {
+ // This revision is known locally. Remember its sequence as the parent of the next one:
+ sequence = localRev.sequence;
+ Assert(sequence > 0);
+ localParentSequence = sequence;
+
} else {
- // It's an intermediate parent, so insert a stub:
- newRev = [[[TDRevision alloc] initWithDocID: docID revID: revID deleted: NO]
- autorelease];
+ // This revision isn't known, so add it:
+ TDRevision* newRev;
+ NSData* json = nil;
+ BOOL current = NO;
+ if (i==0) {
+ // Hey, this is the leaf revision we're inserting:
+ newRev = rev;
+ if (!rev.deleted) {
+ json = [self encodeDocumentJSON: rev];
+ if (!json)
+ return 400;
+ }
+ current = YES;
+ } else {
+ // It's an intermediate parent, so insert a stub:
+ newRev = [[[TDRevision alloc] initWithDocID: docID revID: revID deleted: NO]
+ autorelease];
+ }
+
+ // Insert it:
+ sequence = [self insertRevision: newRev
+ docNumericID: docNumericID
+ parentSequence: sequence
+ current: current
+ JSON: json];
+ if (sequence <= 0)
+ return 500;
+ newRev.sequence = sequence;
+
+ if (i==0) {
+ // Write any changed attachments for the new revision. As the parent sequence use
+ // the latest local revision (this is to copy attachments from):
+ TDStatus status = [self processAttachmentsForRevision: rev
+ withParentSequence: localParentSequence];
+ if (status >= 300)
+ return status;
+ }
}
+ }
- // Insert it:
- sequence = [self insertRevision: newRev
- docNumericID: docNumericID
- parentSequence: sequence
- current: current
- JSON: json];
- if (sequence <= 0)
+ // Mark the latest local rev as no longer current:
+ if (localParentSequence > 0 && localParentSequence != sequence) {
+ if (![_fmdb executeUpdate: @"UPDATE revs SET current=0 WHERE sequence=?",
+ $object(localParentSequence)])
return 500;
- newRev.sequence = sequence;
-
- if (i==0) {
- // Write any changed attachments for the new revision. As the parent sequence use
- // the latest local revision (this is to copy attachments from):
- TDStatus status = [self processAttachmentsForRevision: rev
- withParentSequence: localParentSequence];
- if (status >= 300)
- return status;
- }
}
- }
- // Mark the latest local rev as no longer current:
- if (localParentSequence > 0 && localParentSequence != sequence) {
- if (![_fmdb executeUpdate: @"UPDATE revs SET current=0 WHERE sequence=?",
- $object(localParentSequence)])
- return 500;
+ success = YES;
+ } @finally {
+ [self endTransaction: success];
}
-
+
// Notify and return:
[self notifyChange: rev source: source];
return 201;
View
@@ -35,7 +35,6 @@ typedef BOOL (^TDValidationBlock) (TDRevision* newRevision,
FMDatabase *_fmdb;
BOOL _open;
NSInteger _transactionLevel;
- BOOL _transactionFailed;
NSMutableDictionary* _views;
NSMutableArray* _validations;
TDBlobStore* _attachments;
@@ -52,12 +51,15 @@ typedef BOOL (^TDValidationBlock) (TDRevision* newRevision,
@property (readonly) NSString* path;
@property (readonly) NSString* name;
@property (readonly) BOOL exists;
-@property (readonly) int error;
-- (void) beginTransaction;
-- (void) endTransaction;
-@property BOOL transactionFailed;
+/** Begins a database transaction. Transactions can nest. Every -beginTransaction must be balanced by a later -endTransaction:. */
+- (BOOL) beginTransaction;
+/** Commits or aborts (rolls back) a transaction.
+ @param commit If YES, commits; if NO, aborts and rolls back, undoing all changes made since the matching -beginTransaction call, *including* any committed nested transactions. */
+- (BOOL) endTransaction: (BOOL)commit;
+
+/** Compacts the database storage by removing the bodies and attachments of obsolete revisions. */
- (TDStatus) compact;
// DOCUMENTS:
View
@@ -214,44 +214,28 @@ - (NSString*) name {
return _fmdb.databasePath.lastPathComponent.stringByDeletingPathExtension;
}
-- (int) error {
- return _fmdb.lastErrorCode;
-}
-
-- (NSString*) errorMessage {
- return _fmdb.lastErrorMessage;
-}
-
-- (void) beginTransaction {
- if (++_transactionLevel == 1) {
- LogTo(TDDatabase, @"Begin transaction...");
- [_fmdb beginTransaction];
- _transactionFailed = NO;
- }
+- (BOOL) beginTransaction {
+ if (![_fmdb executeUpdate: $sprintf(@"SAVEPOINT tdb%d", _transactionLevel + 1)])
+ return NO;
+ ++_transactionLevel;
+ LogTo(TDDatabase, @"Begin transaction (level %d)...", _transactionLevel);
+ return YES;
}
-- (void) endTransaction {
+- (BOOL) endTransaction: (BOOL)commit {
Assert(_transactionLevel > 0);
- if (--_transactionLevel == 0) {
- if (_transactionFailed) {
- LogTo(TDDatabase, @"Rolling back failed transaction!");
- [_fmdb rollback];
- } else {
- LogTo(TDDatabase, @"Committing transaction");
- [_fmdb commit];
- }
+ if (commit) {
+ LogTo(TDDatabase, @"Commit transaction (level %d)", _transactionLevel);
+ } else {
+ LogTo(TDDatabase, @"CANCEL transaction (level %d)", _transactionLevel);
+ if (![_fmdb executeUpdate: $sprintf(@"ROLLBACK TO tdb%d", _transactionLevel)])
+ return NO;
}
- _transactionFailed = NO;
-}
-
-- (BOOL) transactionFailed { return _transactionFailed; }
-
-- (void) setTransactionFailed: (BOOL)failed {
- Assert(_transactionLevel > 0);
- Assert(failed, @"Can't clear the transactionFailed property!");
- LogTo(TDDatabase, @"Current transaction failed, will abort!");
- _transactionFailed = failed;
+ if (![_fmdb executeUpdate: $sprintf(@"RELEASE tdb%d", _transactionLevel)])
+ return NO;
+ --_transactionLevel;
+ return YES;
}
@@ -33,7 +33,6 @@
static TDDatabase* createDB(void) {
TDDatabase *db = [TDDatabase createEmptyDBAtPath: kPath];
CAssert([db open]);
- CAssert(![db error]);
return db;
}
View
@@ -235,7 +235,7 @@ - (void) insertRevisions:(NSArray *)revs {
if (maxSequence > self.lastSequence.longLongValue)
self.lastSequence = $sprintf(@"%lld", maxSequence);
- [_db endTransaction];
+ [_db endTransaction: YES];
LogTo(Sync, @"%@ finished inserting %u revisions", self, revs.count);
[self asyncTasksFinished: revs.count];
View
@@ -94,7 +94,7 @@ - (void) removeIndex {
$object(_viewID)];
[_db.fmdb executeUpdate: @"UPDATE views SET lastsequence=0 WHERE view_id=?",
$object(_viewID)];
- [_db endTransaction];
+ [_db endTransaction: YES];
}
@@ -234,11 +234,9 @@ - (TDStatus) updateIndex {
} @finally {
[r close];
- if (status >= 300) {
+ if (status >= 300)
Warn(@"TouchDB: Failed to rebuild view '%@': %d", _name, status);
- _db.transactionFailed = YES;
- }
- [_db endTransaction];
+ [_db endTransaction: (status < 300)];
}
return status;
}

0 comments on commit ccb04b6

Please sign in to comment.