Selaa lähdekoodia

added support for savepoints (a kind of nested transaction)

ccgus 14 vuotta sitten
vanhempi
commit
e2120a6d94
7 muutettua tiedostoa jossa 244 lisäystä ja 49 poistoa
  1. 1 1
      README.markdown
  2. 1 0
      fmdb.xcodeproj/project.pbxproj
  3. 12 6
      src/FMDatabase.h
  4. 119 34
      src/FMDatabase.m
  5. 9 2
      src/FMDatabasePool.h
  6. 44 3
      src/FMDatabasePool.m
  7. 58 3
      src/fmdb.m

+ 1 - 1
README.markdown

@@ -179,7 +179,7 @@ Starting a transaction will keep the db from going back into the pool automatica
 
 There is also a block based transaction approach:
 
-	[dbPool useTransaction:^(FMDatabase *db, BOOL *rollback) {
+	[dbPool inTransaction:^(FMDatabase *db, BOOL *rollback) {
 		[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
 		[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
 		[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];

+ 1 - 0
fmdb.xcodeproj/project.pbxproj

@@ -259,6 +259,7 @@
 		1DEB927508733DD40010E9CD /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				ARCHS = "$(ARCHS_STANDARD_64_BIT)";
 				COPY_PHASE_STRIP = NO;
 				GCC_DYNAMIC_NO_PIC = NO;
 				GCC_ENABLE_FIX_AND_CONTINUE = YES;

+ 12 - 6
src/FMDatabase.h

@@ -9,15 +9,13 @@
 	NSString*           _databasePath;
     BOOL                _logsErrors;
     BOOL                _crashOnErrors;
-    BOOL                _inTransaction;
     BOOL                _traceExecution;
     BOOL                _checkedOut;
     BOOL                _shouldCacheStatements;
-    BOOL                _inUse;
+    BOOL                _isExecutingStatement;
+    BOOL                _inTransaction;
     int                 _busyRetryTimeout;
     
-    
-    
     NSMutableDictionary *_cachedStatements;
 	NSMutableSet        *_openResultSets;
     
@@ -26,7 +24,6 @@
 }
 
 
-@property (assign) BOOL inTransaction;
 @property (assign) BOOL traceExecution;
 @property (assign) BOOL checkedOut;
 @property (assign) int busyRetryTimeout;
@@ -58,6 +55,8 @@
 
 - (int)lastErrorCode;
 - (BOOL)hadError;
+- (NSError*)lastError;
+
 - (sqlite_int64)lastInsertRowId;
 
 - (sqlite3*)sqliteHandle;
@@ -77,10 +76,17 @@
 - (BOOL)commit;
 - (BOOL)beginTransaction;
 - (BOOL)beginDeferredTransaction;
-
+- (BOOL)inTransaction;
 - (BOOL)shouldCacheStatements;
 - (void)setShouldCacheStatements:(BOOL)value;
 
+#if SQLITE_VERSION_NUMBER >= 3007000
+- (BOOL)startSavePointWithName:(NSString*)name error:(NSError**)outErr;
+- (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError**)outErr;
+- (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError**)outErr;
+- (NSError*)inSavePoint:(void (^)(BOOL *rollback))block;
+#endif
+
 + (BOOL)isSQLiteThreadSafe;
 + (NSString*)sqliteLibVersion;
 

+ 119 - 34
src/FMDatabase.m

@@ -4,13 +4,10 @@
 @interface FMDatabase ()
 
 - (void)checkPoolPushBack;
-- (BOOL)inUse;
-- (void)setInUse:(BOOL)b;
 
 @end
 
 @implementation FMDatabase
-@synthesize inTransaction=_inTransaction;
 @synthesize cachedStatements=_cachedStatements;
 @synthesize logsErrors=_logsErrors;
 @synthesize crashOnErrors=_crashOnErrors;
@@ -19,7 +16,6 @@ @implementation FMDatabase
 @synthesize traceExecution=_traceExecution;
 @synthesize pool=_pool;
 
-
 + (id)databaseWithPath:(NSString*)aPath {
     return [[[self alloc] initWithPath:aPath] autorelease];
 }
@@ -287,33 +283,37 @@ - (int)lastErrorCode {
     return sqlite3_errcode(_db);
 }
 
+- (NSError*)lastError {
+    return [NSError errorWithDomain:@"FMDatabase" code:sqlite3_errcode(_db) userInfo:[NSDictionary dictionaryWithObject:[self lastErrorMessage] forKey:NSLocalizedDescriptionKey]];
+}
+
 - (sqlite_int64)lastInsertRowId {
     
-    if (_inUse) {
+    if (_isExecutingStatement) {
         [self warnInUse];
         return NO;
     }
     
-    _inUse = YES;
+    _isExecutingStatement = YES;
     
     sqlite_int64 ret = sqlite3_last_insert_rowid(_db);
     
-    _inUse = NO;
+    _isExecutingStatement = NO;
     
     return ret;
 }
 
 - (int)changes {
-    if (_inUse) {
+    if (_isExecutingStatement) {
         [self warnInUse];
         return 0;
     }
     
-    _inUse = YES;
+    _isExecutingStatement = YES;
     
     int ret = sqlite3_changes(_db);
     
-    _inUse = NO;
+    _isExecutingStatement = NO;
     
     return ret;
 }
@@ -472,13 +472,13 @@ - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arr
         return 0x00;
     }
     
-    if (_inUse) {
+    if (_isExecutingStatement) {
         [self warnInUse];
         [self checkPoolPushBack];
         return 0x00;
     }
     
-    [self setInUse:YES];
+    _isExecutingStatement = YES;
     
     int rc                  = 0x00;
     sqlite3_stmt *pStmt     = 0x00;
@@ -510,7 +510,7 @@ - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arr
                     NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]);
                     NSLog(@"Database busy");
                     sqlite3_finalize(pStmt);
-                    [self setInUse:NO];
+                    _isExecutingStatement = NO;
                     [self checkPoolPushBack];
                     return nil;
                 }
@@ -528,7 +528,7 @@ - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arr
                 }
                 
                 sqlite3_finalize(pStmt);
-                [self setInUse:NO];
+                _isExecutingStatement = NO;
                 [self checkPoolPushBack];
                 return nil;
             }
@@ -561,7 +561,7 @@ - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arr
     if (idx != queryCount) {
         NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)");
         sqlite3_finalize(pStmt);
-        [self setInUse:NO];
+        _isExecutingStatement = NO;
         [self checkPoolPushBack];
         return nil;
     }
@@ -588,7 +588,7 @@ - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arr
     
     [statement release];    
     
-    [self setInUse:NO];
+    _isExecutingStatement = NO;
     
     return rs;
 }
@@ -627,13 +627,13 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra
         return NO;
     }
     
-    if (_inUse) {
+    if (_isExecutingStatement) {
         [self checkPoolPushBack];
         [self warnInUse];
         return NO;
     }
     
-    [self setInUse:YES];
+    _isExecutingStatement = YES;
     
     int rc                   = 0x00;
     sqlite3_stmt *pStmt      = 0x00;
@@ -664,7 +664,7 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra
                     NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]);
                     NSLog(@"Database busy");
                     sqlite3_finalize(pStmt);
-                    [self setInUse:NO];
+                    _isExecutingStatement = NO;
                     [self checkPoolPushBack];
                     return NO;
                 }
@@ -687,7 +687,7 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra
                     *outErr = [NSError errorWithDomain:[NSString stringWithUTF8String:sqlite3_errmsg(_db)] code:rc userInfo:nil];
                 }
                 
-                [self setInUse:NO];
+                _isExecutingStatement = NO;
                 [self checkPoolPushBack];
                 return NO;
             }
@@ -720,7 +720,7 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra
     if (idx != queryCount) {
         NSLog(@"Error: the bind count is not correct for the # of variables (%@) (executeUpdate)", sql);
         sqlite3_finalize(pStmt);
-        [self setInUse:NO];
+        _isExecutingStatement = NO;
         [self checkPoolPushBack];
         return NO;
     }
@@ -796,7 +796,7 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra
         rc = sqlite3_finalize(pStmt);
     }
     
-    [self setInUse:NO];
+    _isExecutingStatement = NO;
     [self checkPoolPushBack];
     return (rc == SQLITE_OK);
 }
@@ -841,24 +841,26 @@ - (BOOL)update:(NSString*)sql error:(NSError**)outErr bind:(id)bindArgs, ... {
 }
 
 - (BOOL)rollback {
-    BOOL b = [self executeUpdate:@"ROLLBACK TRANSACTION;"];
+    BOOL b = [self executeUpdate:@"rollback transaction"];
+    
+    [self pushToPool];
+    
     if (b) {
         _inTransaction = NO;
     }
     
-    [self pushToPool];
-    
     return b;
 }
 
 - (BOOL)commit {
-    BOOL b =  [self executeUpdate:@"COMMIT TRANSACTION;"];
+    BOOL b =  [self executeUpdate:@"commit transaction"];
+    
+    [self pushToPool];
+    
     if (b) {
         _inTransaction = NO;
     }
     
-    [self pushToPool];
-    
     return b;
 }
 
@@ -868,7 +870,7 @@ - (BOOL)beginDeferredTransaction {
         [self popFromPool];
     }
     
-    BOOL b =  [self executeUpdate:@"BEGIN DEFERRED TRANSACTION;"];
+    BOOL b = [self executeUpdate:@"begin deferred transaction"];
     if (b) {
         _inTransaction = YES;
     }
@@ -882,7 +884,7 @@ - (BOOL)beginTransaction {
         [self popFromPool];
     }
     
-    BOOL b =  [self executeUpdate:@"BEGIN EXCLUSIVE TRANSACTION;"];
+    BOOL b = [self executeUpdate:@"begin exclusive transaction"];
     if (b) {
         _inTransaction = YES;
     }
@@ -890,14 +892,96 @@ - (BOOL)beginTransaction {
     return b;
 }
 
-- (BOOL)inUse {
-    return _inUse || _inTransaction;
+- (BOOL)inTransaction {
+    return _inTransaction;
 }
 
-- (void)setInUse:(BOOL)b {
-    _inUse = b;
+#if SQLITE_VERSION_NUMBER >= 3007000
+
+- (BOOL)startSavePointWithName:(NSString*)name error:(NSError**)outErr {
+    
+    // FIXME: make sure the savepoint name doesn't have a ' in it.
+    
+    NSAssert(name, @"Missing name for a savepoint", nil);
+    
+    if (_pool) {
+        [self popFromPool];
+    }
+    
+    if (![self executeUpdate:[NSString stringWithFormat:@"savepoint '%@';", name]]) {
+        
+        if (*outErr) {
+            *outErr = [self lastError];
+        }
+        
+        return NO;
+    }
+    
+    return YES;
 }
 
+- (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError**)outErr {
+    
+    NSAssert(name, @"Missing name for a savepoint", nil);
+    
+    BOOL worked = [self executeUpdate:[NSString stringWithFormat:@"release savepoint '%@';", name]];
+    
+    if (!worked && *outErr) {
+        *outErr = [self lastError];
+    }
+    
+    if (_pool) {
+        [self pushToPool];
+    }
+    
+    return worked;
+}
+
+- (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError**)outErr {
+    
+    NSAssert(name, @"Missing name for a savepoint", nil);
+    
+    BOOL worked = [self executeUpdate:[NSString stringWithFormat:@"rollback transaction to savepoint '%@';", name]];
+    
+    if (!worked && *outErr) {
+        *outErr = [self lastError];
+    }
+    
+    if (_pool) {
+        [self pushToPool];
+    }
+    
+    return worked;
+}
+
+- (NSError*)inSavePoint:(void (^)(BOOL *rollback))block {
+    static unsigned long savePointIdx = 0;
+    
+    NSString *name = [NSString stringWithFormat:@"dbSavePoint%ld", savePointIdx++];
+    
+    BOOL shouldRollback = NO;
+    
+    NSError *err = 0x00;
+    
+    if (![self startSavePointWithName:name error:&err]) {
+        return err;
+    }
+    
+    block(&shouldRollback);
+    
+    if (shouldRollback) {
+        [self rollbackToSavePointWithName:name error:&err];
+    }
+    else {
+        [self releaseSavePointWithName:name error:&err];
+    }
+    
+    return err;
+}
+
+#endif
+
+
 - (BOOL)shouldCacheStatements {
     return _shouldCacheStatements;
 }
@@ -915,6 +999,7 @@ - (void)setShouldCacheStatements:(BOOL)value {
     }
 }
 
+
 - (FMDatabase*)popFromPool {
     
     if (!_pool) {

+ 9 - 2
src/FMDatabasePool.h

@@ -38,9 +38,16 @@
 - (NSUInteger)countOfOpenDatabases;
 - (void)releaseAllDatabases;
 
-- (void)useDatabase:(void (^)(FMDatabase *db))block;
+- (void)inDatabase:(void (^)(FMDatabase *db))block;
 
-- (void)useTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;
+- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;
+- (void)inDeferredTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;
+
+#if SQLITE_VERSION_NUMBER >= 3007000
+// NOTE: you can not nest these, since calling it will pull another database out of the pool and you'll get a deadlock.
+// If you need to nest, use FMDatabase's startSavePointWithName:error: instead.
+- (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block;
+#endif
 
 @end
 

+ 44 - 3
src/FMDatabasePool.m

@@ -154,7 +154,7 @@ - (void)releaseAllDatabases {
     }];
 }
 
-- (void)useDatabase:(void (^)(FMDatabase *db))block {
+- (void)inDatabase:(void (^)(FMDatabase *db))block {
     
     FMDatabase *db = [[self db] popFromPool];
     
@@ -163,13 +163,19 @@ - (void)useDatabase:(void (^)(FMDatabase *db))block {
     [db pushToPool];
 }
 
-- (void)useTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block {
+- (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block {
     
     BOOL shouldRollback = NO;
     
     FMDatabase *db = [self db];
     
-    [db beginTransaction];
+    if (useDeferred) {
+        [db beginDeferredTransaction];
+    }
+    else {
+        [db beginTransaction];
+    }
+    
     
     block(db, &shouldRollback);
     
@@ -181,6 +187,41 @@ - (void)useTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block {
     }
 }
 
+- (void)inDeferredTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block {
+    [self beginTransaction:YES withBlock:block];
+}
+
+- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block {
+    [self beginTransaction:NO withBlock:block];
+}
+
+- (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block {
+    
+    static unsigned long savePointIdx = 0;
+    
+    NSString *name = [NSString stringWithFormat:@"savePoint%ld", savePointIdx++];
+    
+    BOOL shouldRollback = NO;
+    
+    FMDatabase *db = [self db];
+    
+    NSError *err = 0x00;
+    
+    if (![db startSavePointWithName:name error:&err]) {
+        return err;
+    }
+    
+    block(db, &shouldRollback);
+    
+    if (shouldRollback) {
+        [db rollbackToSavePointWithName:name error:&err];
+    }
+    else {
+        [db releaseSavePointWithName:name error:&err];
+    }
+    
+    return err;
+}
 
 
 @end

+ 58 - 3
src/fmdb.m

@@ -567,7 +567,7 @@ int main (int argc, const char * argv[]) {
     
     FMDBQuickCheck([dbPool countOfOpenDatabases] == 0);
     
-    [dbPool useDatabase:^(FMDatabase *aDb) {
+    [dbPool inDatabase:^(FMDatabase *aDb) {
         
         FMDBQuickCheck([dbPool countOfCheckedInDatabases] == 0);
         FMDBQuickCheck([dbPool countOfCheckedOutDatabases] == 1);
@@ -685,7 +685,7 @@ int main (int argc, const char * argv[]) {
     FMDBQuickCheck([dbPool countOfOpenDatabases] == 1);
     
     
-    [dbPool useTransaction:^(FMDatabase *adb, BOOL *rollback) {
+    [dbPool inTransaction:^(FMDatabase *adb, BOOL *rollback) {
         [adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1001]];
         [adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1002]];
         [adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1003]];
@@ -706,7 +706,7 @@ int main (int argc, const char * argv[]) {
     FMDBQuickCheck(![rs2 next]);
     
     
-    [dbPool useTransaction:^(FMDatabase *adb, BOOL *rollback) {
+    [dbPool inDeferredTransaction:^(FMDatabase *adb, BOOL *rollback) {
         [adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1004]];
         [adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1005]];
         
@@ -725,6 +725,61 @@ int main (int argc, const char * argv[]) {
     
     FMDBQuickCheck([dbPool countOfOpenDatabases] == 1);
     
+    
+    err = [dbPool inSavePoint:^(FMDatabase *db, BOOL *rollback) {
+        FMDBQuickCheck(![adb hadError]);
+        [adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1006]];
+    }];
+    
+    rs2 = [[dbPool db] executeQuery:@"select * from easy where a = ?", [NSNumber numberWithInt:1006]];
+    FMDBQuickCheck([rs2 next]);
+    [rs2 close];
+    
+    {
+        db = [dbPool db];
+        FMDBQuickCheck([db startSavePointWithName:@"a" error:nil]);
+        
+        [db executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1007]];
+        
+        FMDBQuickCheck([db startSavePointWithName:@"b" error:nil]);
+        
+        FMDBQuickCheck(([db executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1008]]));
+        
+        FMDBQuickCheck([db releaseSavePointWithName:@"b" error:nil]);
+        
+        FMDBQuickCheck([db releaseSavePointWithName:@"a" error:nil]);
+        
+        rs2 = [[dbPool db] executeQuery:@"select * from easy where a = ?", [NSNumber numberWithInt:1007]];
+        FMDBQuickCheck([rs2 next]);
+        FMDBQuickCheck(![rs2 next]); // close it out.
+        
+        rs2 = [[dbPool db] executeQuery:@"select * from easy where a = ?", [NSNumber numberWithInt:1008]];
+        FMDBQuickCheck([rs2 next]);
+        FMDBQuickCheck(![rs2 next]); // close it out.
+    }
+    
+    
+    {
+            
+        err = [dbPool inSavePoint:^(FMDatabase *adb, BOOL *rollback) {
+            FMDBQuickCheck(![adb hadError]);
+            [adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1009]];
+            
+            [adb inSavePoint:^(BOOL *rollback) {
+                FMDBQuickCheck(([adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1010]]));
+                *rollback = YES;
+            }];
+        }];
+        
+        rs2 = [[dbPool db] executeQuery:@"select * from easy where a = ?", [NSNumber numberWithInt:1009]];
+        FMDBQuickCheck([rs2 next]);
+        FMDBQuickCheck(![rs2 next]); // close it out.
+        
+        rs2 = [[dbPool db] executeQuery:@"select * from easy where a = ?", [NSNumber numberWithInt:1010]];
+        FMDBQuickCheck(![rs2 next]);
+        
+    }
+    
     NSLog(@"That was version %@ of sqlite", [FMDatabase sqliteLibVersion]);
     
     [pool release];