Browse Source

So, this is an attempt to make FMDatabase thread safe. Fun times!

August Mueller 14 years ago
parent
commit
7c2141e5b9
3 changed files with 78 additions and 117 deletions
  1. 3 5
      src/FMDatabase.h
  2. 59 94
      src/FMDatabase.m
  3. 16 18
      src/FMDatabaseAdditions.m

+ 3 - 5
src/FMDatabase.h

@@ -8,7 +8,6 @@
 	NSString*   databasePath;
     BOOL        logsErrors;
     BOOL        crashOnErrors;
-    BOOL        inUse;
     BOOL        inTransaction;
     BOOL        traceExecution;
     BOOL        checkedOut;
@@ -16,6 +15,9 @@
     BOOL        shouldCacheStatements;
     NSMutableDictionary *cachedStatements;
 	NSMutableSet *openResultSets;
+    
+    dispatch_queue_t _lockQueue;
+    
 }
 
 
@@ -71,10 +73,6 @@
 - (BOOL)beginTransaction;
 - (BOOL)beginDeferredTransaction;
 
-- (BOOL)inUse;
-- (void)setInUse:(BOOL)value;
-
-
 - (BOOL)shouldCacheStatements;
 - (void)setShouldCacheStatements:(BOOL)value;
 

+ 59 - 94
src/FMDatabase.m

@@ -14,6 +14,15 @@ + (id)databaseWithPath:(NSString*)aPath {
     return [[[self alloc] initWithPath:aPath] autorelease];
 }
 
++ (NSString*)sqliteLibVersion {
+    return [NSString stringWithFormat:@"%s", sqlite3_libversion()];
+}
+
++ (BOOL)isThreadSafe {
+    // make sure to read the sqlite headers on this guy!
+    return sqlite3_threadsafe();
+}
+
 - (id)initWithPath:(NSString*)aPath {
     self = [super init];
     
@@ -24,6 +33,7 @@ - (id)initWithPath:(NSString*)aPath {
         logsErrors          = 0x00;
         crashOnErrors       = 0x00;
         busyRetryTimeout    = 0x00;
+        _lockQueue          = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
     }
     
     return self;
@@ -41,11 +51,16 @@ - (void)dealloc {
     [cachedStatements release];
     [databasePath release];
     
+    if (_lockQueue) {
+        dispatch_release(_lockQueue);
+        _lockQueue = 0x00;
+    }
+    
     [super dealloc];
 }
 
-+ (NSString*)sqliteLibVersion {
-    return [NSString stringWithFormat:@"%s", sqlite3_libversion()];
+- (void)executeLocked:(void (^)(void))aBlock {
+    dispatch_sync(_lockQueue, aBlock);
 }
 
 - (NSString *)databasePath {
@@ -99,9 +114,12 @@ - (BOOL)close {
     do {
         retry   = NO;
         rc      = sqlite3_close(db);
+        
         if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
+            
             retry = YES;
             usleep(20);
+            
             if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) {
                 NSLog(@"%s:%d", __FUNCTION__, __LINE__);
                 NSLog(@"Database busy, unable to close");
@@ -129,34 +147,38 @@ - (BOOL)close {
 
 - (void)clearCachedStatements {
     
-    NSEnumerator *e = [cachedStatements objectEnumerator];
-    FMStatement *cachedStmt;
-
-    while ((cachedStmt = [e nextObject])) {
-        [cachedStmt close];
-    }
-    
-    [cachedStatements removeAllObjects];
+    [self executeLocked:^() {
+        
+        for (FMStatement *cachedStmt in [cachedStatements objectEnumerator]) {
+            NSLog(@"cachedStmt: '%@'", cachedStmt);
+            [cachedStmt close];
+        }
+        
+        [cachedStatements removeAllObjects];
+    }];
 }
 
 - (void)closeOpenResultSets {
-    //Copy the set so we don't get mutation errors
-    NSSet *resultSets = [[openResultSets copy] autorelease];
     
-    NSEnumerator *e = [resultSets objectEnumerator];
-    NSValue *returnedResultSet = nil;
-    
-    while((returnedResultSet = [e nextObject])) {
-        FMResultSet *rs = (FMResultSet *)[returnedResultSet pointerValue];
-        if ([rs respondsToSelector:@selector(close)]) {
+    [self executeLocked:^() {
+        //Copy the set so we don't get mutation errors
+        for (NSValue *rsInWrappedInATastyValueMeal in [[openResultSets copy] autorelease]) {
+            FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
+            
+            [rs setParentDB:nil];
             [rs close];
+            
+            [openResultSets removeObject:rsInWrappedInATastyValueMeal];
         }
-    }
+    }];
 }
 
 - (void)resultSetDidClose:(FMResultSet *)resultSet {
     NSValue *setValue = [NSValue valueWithNonretainedObject:resultSet];
-    [openResultSets removeObject:setValue];
+    
+    [self executeLocked:^() {
+        [openResultSets removeObject:setValue];
+    }];
 }
 
 - (FMStatement*)cachedStatementForQuery:(NSString*)query {
@@ -165,9 +187,15 @@ - (FMStatement*)cachedStatementForQuery:(NSString*)query {
 
 - (void)setCachedStatement:(FMStatement*)statement forQuery:(NSString*)query {
     //NSLog(@"setting query: %@", query);
+    
     query = [query copy]; // in case we got handed in a mutable string...
+    
     [statement setQuery:query];
-    [cachedStatements setObject:statement forKey:query];
+    
+    [self executeLocked:^() {
+        [cachedStatements setObject:statement forKey:query];
+    }];
+    
     [query release];
 }
 
@@ -264,31 +292,11 @@ - (int)lastErrorCode {
 }
 
 - (sqlite_int64)lastInsertRowId {
-    
-    if (inUse) {
-        [self warnInUse];
-        return NO;
-    }
-    [self setInUse:YES];
-    
-    sqlite_int64 ret = sqlite3_last_insert_rowid(db);
-    
-    [self setInUse:NO];
-    
-    return ret;
+    return sqlite3_last_insert_rowid(db);
 }
 
 - (int)changes {
-    if (inUse) {
-        [self warnInUse];
-        return 0;
-    }
-    
-    [self setInUse:YES];
-    int ret = sqlite3_changes(db);
-    [self setInUse:NO];
-    
-    return ret;
+    return sqlite3_changes(db);
 }
 
 - (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt {
@@ -437,7 +445,6 @@ - (void)_extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMu
         }
         last = current;
     }
-    
 }
 
 - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args {
@@ -446,18 +453,10 @@ - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arr
         return 0x00;
     }
     
-    if (inUse) {
-        [self warnInUse];
-        return 0x00;
-    }
-    
-    [self setInUse:YES];
-    
-    FMResultSet *rs = nil;
-    
     int rc                  = 0x00;
     sqlite3_stmt *pStmt     = 0x00;
     FMStatement *statement  = 0x00;
+    FMResultSet *rs         = 0x00;
     
     if (traceExecution && sql) {
         NSLog(@"%@ executeQuery: %@", self, sql);
@@ -484,13 +483,11 @@ - (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];
                     return nil;
                 }
             }
             else if (SQLITE_OK != rc) {
                 
-                
                 if (logsErrors) {
                     NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
                     NSLog(@"DB Query: %@", sql);
@@ -503,7 +500,6 @@ - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arr
                 
                 sqlite3_finalize(pStmt);
                 
-                [self setInUse:NO];
                 return nil;
             }
         }
@@ -535,7 +531,6 @@ - (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];
         return nil;
     }
     
@@ -553,15 +548,14 @@ - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arr
     // the statement gets closed in rs's dealloc or [rs close];
     rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];
     [rs setQuery:sql];
+    
     NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
     [openResultSets addObject:openResultSet];
     
-    statement.useCount = statement.useCount + 1;
+    [statement setUseCount:[statement useCount] + 1];
     
     [statement release];    
     
-    [self setInUse:NO];
-    
     return rs;
 }
 
@@ -598,13 +592,6 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra
         return NO;
     }
     
-    if (inUse) {
-        [self warnInUse];
-        return NO;
-    }
-    
-    [self setInUse:YES];
-    
     int rc                   = 0x00;
     sqlite3_stmt *pStmt      = 0x00;
     FMStatement *cachedStmt  = 0x00;
@@ -634,13 +621,11 @@ - (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];
                     return NO;
                 }
             }
             else if (SQLITE_OK != rc) {
                 
-                
                 if (logsErrors) {
                     NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
                     NSLog(@"DB Query: %@", sql);
@@ -652,7 +637,6 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra
                 }
                 
                 sqlite3_finalize(pStmt);
-                [self setInUse:NO];
                 
                 if (outErr) {
                     *outErr = [NSError errorWithDomain:[NSString stringWithUTF8String:sqlite3_errmsg(db)] code:rc userInfo:nil];
@@ -664,7 +648,6 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra
         while (retry);
     }
     
-    
     id obj;
     int idx = 0;
     int queryCount = sqlite3_bind_parameter_count(pStmt);
@@ -678,7 +661,6 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra
             obj = va_arg(args, id);
         }
         
-        
         if (traceExecution) {
             NSLog(@"obj: %@", obj);
         }
@@ -691,7 +673,6 @@ - (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];
         return NO;
     }
     
@@ -699,6 +680,7 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra
      ** executed is not a SELECT statement, we assume no data will be returned.
      */
     numberOfRetries = 0;
+    
     do {
         rc      = sqlite3_step(pStmt);
         retry   = NO;
@@ -755,7 +737,7 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra
     }
     
     if (cachedStmt) {
-        cachedStmt.useCount = cachedStmt.useCount + 1;
+        [cachedStmt setUseCount:[cachedStmt useCount] + 1];
         rc = sqlite3_reset(pStmt);
     }
     else {
@@ -765,8 +747,6 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra
         rc = sqlite3_finalize(pStmt);
     }
     
-    [self setInUse:NO];
-    
     return (rc == SQLITE_OK);
 }
 
@@ -781,8 +761,6 @@ - (BOOL)executeUpdate:(NSString*)sql, ... {
     return result;
 }
 
-
-
 - (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments {
     return [self executeUpdate:sql error:nil withArgumentsInArray:arguments orVAList:nil];
 }
@@ -791,8 +769,9 @@ - (BOOL)executeUpdateWithFormat:(NSString*)format, ... {
     va_list args;
     va_start(args, format);
     
-    NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]];
+    NSMutableString *sql      = [NSMutableString stringWithCapacity:[format length]];
     NSMutableArray *arguments = [NSMutableArray array];
+    
     [self _extractSQL:format argumentsList:args intoString:sql arguments:arguments];    
     
     va_end(args);
@@ -842,17 +821,6 @@ - (BOOL)beginTransaction {
     return b;
 }
 
-
-
-- (BOOL)inUse {
-    return inUse || inTransaction;
-}
-
-- (void)setInUse:(BOOL)b {
-    inUse = b;
-}
-
-
 - (BOOL)shouldCacheStatements {
     return shouldCacheStatements;
 }
@@ -870,10 +838,7 @@ - (void)setShouldCacheStatements:(BOOL)value {
     }
 }
 
-+ (BOOL)isThreadSafe {
-    // make sure to read the sqlite headers on this guy!
-    return sqlite3_threadsafe();
-}
+
 
 @end
 

+ 16 - 18
src/FMDatabaseAdditions.m

@@ -52,24 +52,25 @@ - (NSDate*)dateForQuery:(NSString*)query, ... {
 }
 
 
-//check if table exist in database (patch from OZLB)
 - (BOOL)tableExists:(NSString*)tableName {
     
-    BOOL returnBool;
-    //lower case table name
     tableName = [tableName lowercaseString];
-    //search in sqlite_master table if table exists
+    
     FMResultSet *rs = [self executeQuery:@"select [sql] from sqlite_master where [type] = 'table' and lower(name) = ?", tableName];
+    
     //if at least one next exists, table exists
-    returnBool = [rs next];
+    BOOL returnBool = [rs next];
+    
     //close and free object
     [rs close];
     
     return returnBool;
 }
 
-//get table with list of tables: result colums: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING]
-//check if table exist in database  (patch from OZLB)
+/*
+ get table with list of tables: result colums: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING]
+ check if table exist in database  (patch from OZLB)
+*/
 - (FMResultSet*)getSchema {
     
     //result colums: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING]
@@ -78,7 +79,9 @@ - (FMResultSet*)getSchema {
     return rs;
 }
 
-//get table schema: result colums: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER]
+/* 
+ get table schema: result colums: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER]
+*/
 - (FMResultSet*)getTableSchema:(NSString*)tableName {
     
     //result colums: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER]
@@ -88,16 +91,15 @@ - (FMResultSet*)getTableSchema:(NSString*)tableName {
 }
 
 
-//check if column exist in table
 - (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName {
     
     BOOL returnBool = NO;
-    //lower case table name
-    tableName = [tableName lowercaseString];
-    //lower case column name
+    
+    tableName  = [tableName lowercaseString];
     columnName = [columnName lowercaseString];
-    //get table schema
-    FMResultSet *rs = [self getTableSchema: tableName];
+    
+    FMResultSet *rs = [self getTableSchema:tableName];
+    
     //check if column is present in table schema
     while ([rs next]) {
         if ([[[rs stringForColumn:@"name"] lowercaseString] isEqualToString: columnName]) {
@@ -105,8 +107,6 @@ - (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName {
             break;
         }
     }
-    //close and free object
-    [rs close];
     
     return returnBool;
 }
@@ -117,7 +117,6 @@ - (BOOL)validateSQL:(NSString*)sql error:(NSError**)error {
     BOOL keepTrying = YES;
     int numberOfRetries = 0;
     
-    [self setInUse:YES];
     while (keepTrying == YES) {
         keepTrying = NO;
         int rc = sqlite3_prepare_v2(db, [sql UTF8String], -1, &pStmt, 0);
@@ -141,7 +140,6 @@ - (BOOL)validateSQL:(NSString*)sql error:(NSError**)error {
         }
     }
     
-    [self setInUse:NO];
     sqlite3_finalize(pStmt);
     
     return validationSucceeded;