diff --git a/CHANGES_AND_TODO_LIST.txt b/CHANGES_AND_TODO_LIST.txt index afca5c0f..f6fc857b 100644 --- a/CHANGES_AND_TODO_LIST.txt +++ b/CHANGES_AND_TODO_LIST.txt @@ -3,6 +3,22 @@ Zip, nada, zilch. Got any ideas? If you would like to contribute some code- awesome! I just ask that you make it conform to the coding conventions already set in here, and to add a couple of tests for your new code to fmdb.m. And of course, the code should be of general use to more than just a couple of folks. Send your patches to gus@flyingmeat.com. +2012.02.10: + Changed up FMDatabasePool so that you can't "pop" it from a pool anymore. I just consider this too risky- use the block based functions instead. + Also provided a good reason in main.m for why you should use FMDatabaseQueue instead. Search for "ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS". + I consider this branch 2.0 at this point- I'll let it bake for a couple of days, then push it to the main repo. + + +2012.01.06: + Added a new method to FMDatabase to make custom functions out of a block: + - (void)makeFunctionNamed:(NSString*)name maximumArguments:(int)count withBlock:(void (^)(sqlite3_context *context, int argc, sqlite3_value **argv))block + + Check out the function "testSQLiteFunction" in main.m for an example. + +2011.07.14: + Added methods for named parameters, using keys from an NSDictionary (Thanks to Drarok Ithaqua for the patches!) + Changed FMDatabase's "- (BOOL)update:(NSString*)sql error:(NSError**)outErr bind:(id)bindArgs, ... " to "- (BOOL)update:(NSString*)sql withErrorAndBindings:(NSError**)outErr, ..." as the previous method didn't actually work as advertised in the way it was written. Thanks to @jaekwon for pointing this out. + 2011.06.22 Changed some methods to properties. Hello 2011. Added a warning when you try and use a database that wasn't opened. Hacked together based on patches from Drarok Ithaqua. @@ -10,18 +26,18 @@ If you would like to contribute some code- awesome! I just ask that you make it Added + (BOOL)isThreadSafe to FMDatabase. It'll let you know if the version of SQLite you are running is compiled with it's thread safe options. THIS DOES NOT MEAN FMDATABASE IS THREAD SAFE. I haven't done a review of it for this case, so I'm just saying. 2011.04.09 - Added a method to validate a SQL statement. - Added a method to retrieve the number of columns in a result set. - Added two methods to execute queries and updates with NSString-style format specifiers. + Added a method to validate a SQL statement. + Added a method to retrieve the number of columns in a result set. + Added two methods to execute queries and updates with NSString-style format specifiers. Thanks to Dave DeLong for the patches! 2011.03.12 - Added compatibility with garbage collection. - When an FMDatabase is closed, all open FMResultSets pertaining to that database are also closed. + Added compatibility with garbage collection. + When an FMDatabase is closed, all open FMResultSets pertaining to that database are also closed. Added: - (id) objectForColumnIndex:(int)columnIdx; - (id) objectForColumnName:(NSString*)columnName; - Changes by Dave DeLong. + Changes by Dave DeLong. 2011.02.05 The -(int)changes; method on FMDatabase is a bit more robust now, and there's a new static library target. And if a database path is nil, we now open up a :memory: database. Patch from Pascal Pfiffner! diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 2ee25e0f..dfbccffe 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -30,5 +30,6 @@ Pascal Pfiffner Dave DeLong Drarok Ithaqua Chris Dolan +Sriram Patil Aaaaannnd, Gus Mueller (that's me!) diff --git a/README.markdown b/README.markdown index 5e23e286..77930eab 100644 --- a/README.markdown +++ b/README.markdown @@ -1,17 +1,24 @@ # FMDB - This is an Objective-C wrapper around SQLite: http://sqlite.org/ -## Mailing List: +## The FMDB Mailing List: http://groups.google.com/group/fmdb -## Usage +## Read the SQLite FAQ: +http://www.sqlite.org/faq.html + +Since FMDB is built on top of SQLite, you're going to want to read this page top to bottom at least once. And while you're there, make sure to bookmark the SQLite Documentation page: http://www.sqlite.org/docs.html -There are two main classes in FMDB: +## Automatic Reference Counting (ARC) or Manual Memory Management? +You can use either style in your Cocoa project. FMDB Will figure out which you are using at compile time and do the right thing. + +## Usage +There are three main classes in FMDB: 1. `FMDatabase` - Represents a single SQLite database. Used for executing SQL statements. 2. `FMResultSet` - Represents the results of executing a query on an `FMDatabase`. +3. `FMDatabaseQueue` - If you're wanting to perform queries and updates on multiple threads, you'll want to use this class. It's described in the "Thread Safety" section below. ### Database Creation An `FMDatabase` is created with a path to a SQLite database file. This path can be one of these three: @@ -91,7 +98,16 @@ When providing a SQL statement to FMDB, you should not attempt to "sanitize" any INSERT INTO myTable VALUES (?, ?, ?) -The `?` character is recognized by SQLite as a placeholder for a value to be inserted. The execution methods all accept a variable number of arguments (or a representation of those arguments, such as an `NSArray` or a `va_list`), which are properly escaped for you. +The `?` character is recognized by SQLite as a placeholder for a value to be inserted. The execution methods all accept a variable number of arguments (or a representation of those arguments, such as an `NSArray`, `NSDictionary`, or a `va_list`), which are properly escaped for you. + +Alternatively, you may use named parameters syntax: + + INSERT INTO myTable VALUES (:id, :name, :value) + +The parameters *must* start with a colon. SQLite itself supports other characters, but internally the Dictionary keys are prefixed with a colon, do **not** include the colon in your dictionary keys. + + NSDictionary *argsDict = [NSDictionary dictionaryWithObjectsAndKeys:@"My Name", @"name", nil]; + [db executeUpdate:@"INSERT INTO myTable (name) VALUES (:name)" withArgumentsInDictionary:argsDict]; Thus, you SHOULD NOT do this (or anything like this): @@ -115,6 +131,54 @@ Alternatively, you can use the `-execute*WithFormat:` variant to use `NSString`- Internally, the `-execute*WithFormat:` methods are properly boxing things for you. The following percent modifiers are recognized: `%@`, `%c`, `%s`, `%d`, `%D`, `%i`, `%u`, `%U`, `%hi`, `%hu`, `%qi`, `%qu`, `%f`, `%g`, `%ld`, `%lu`, `%lld`, and `%llu`. Using a modifier other than those will have unpredictable results. If, for some reason, you need the `%` character to appear in your SQL statement, you should use `%%`. + +

Using FMDatabaseQueue and Thread Safety.

+ +Using a single instance of FMDatabase from multiple threads at once is a bad idea. It has always been OK to make a FMDatabase object *per thread*. Just don't share a single instance across threads, and definitely not across multiple threads at the same time. Bad things will eventually happen and you'll eventually get something to crash, or maybe get an exception, or maybe meteorites will fall out of the sky and hit your Mac Pro. *This would suck*. + +**So don't instantiate a single FMDatabase object and use it across multiple threads.** + +Instead, use FMDatabaseQueue. It's your friend and it's here to help. Here's how to use it: + +First, make your queue. + + FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath]; + +Then use it like so: + + [queue inDatabase:^(FMDatabase *db) { + [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]]; + + FMResultSet *rs = [db executeQuery:@"select * from foo"]; + while ([rs next]) { + … + } + }]; + +An easy way to wrap things up in a transaction can be done like this: + + [queue 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]]; + + if (whoopsSomethingWrongHappened) { + *rollback = YES; + return; + } + // etc… + [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:4]]; + }]; + + +FMDatabaseQueue will make a serialized GCD queue in the background and execute the blocks you pass to the GCD queue. This means if you call your FMDatabaseQueue's methods from multiple threads at the same time GDC will execute them in the order they are received. This means queries and updates won't step on each other's toes, and every one is happy. + +## Making custom sqlite functions, based on blocks. + +You can do this! For an example, look for "makeFunctionNamed:" in main.m + ## History The history and changes are availbe on its [GitHub page](https://github.com/ccgus/fmdb) and are summarized in the "CHANGES_AND_TODO_LIST.txt" file. diff --git a/fmdb.xcodeproj/project.pbxproj b/fmdb.xcodeproj/project.pbxproj index 04b4bccc..e758aaea 100644 --- a/fmdb.xcodeproj/project.pbxproj +++ b/fmdb.xcodeproj/project.pbxproj @@ -3,13 +3,20 @@ archiveVersion = 1; classes = { }; - objectVersion = 45; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 8DD76F9C0486AA7600D96B5E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08FB779EFE84155DC02AAC07 /* Foundation.framework */; }; 8DD76F9F0486AA7600D96B5E /* fmdb.1 in CopyFiles */ = {isa = PBXBuildFile; fileRef = C6859EA3029092ED04C91782 /* fmdb.1 */; }; + CC47A00F148581E9002CCDAB /* FMDatabaseQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = CC47A00D148581E9002CCDAB /* FMDatabaseQueue.h */; }; + CC47A010148581E9002CCDAB /* FMDatabaseQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = CC47A00E148581E9002CCDAB /* FMDatabaseQueue.m */; }; + CC47A011148581E9002CCDAB /* FMDatabaseQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = CC47A00E148581E9002CCDAB /* FMDatabaseQueue.m */; }; CC50F2CD0DF9183600E4AAAE /* FMDatabaseAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CC50F2CB0DF9183600E4AAAE /* FMDatabaseAdditions.m */; }; + CC9E4EB913B31188005F9210 /* FMDatabasePool.m in Sources */ = {isa = PBXBuildFile; fileRef = CC9E4EB813B31188005F9210 /* FMDatabasePool.m */; }; + CC9E4EBA13B31188005F9210 /* FMDatabasePool.h in Headers */ = {isa = PBXBuildFile; fileRef = CC9E4EB713B31188005F9210 /* FMDatabasePool.h */; }; + CC9E4EBB13B31188005F9210 /* FMDatabasePool.m in Sources */ = {isa = PBXBuildFile; fileRef = CC9E4EB813B31188005F9210 /* FMDatabasePool.m */; }; + CCBE26C113B3BA8C006F6C37 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CCBE26C013B3BA8C006F6C37 /* AppKit.framework */; }; CCBEBDAC0DF5DE1A003DDD08 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CCBEBDAB0DF5DE1A003DDD08 /* libsqlite3.dylib */; }; CCC24EC10A13E34D00A6D3E3 /* FMDatabase.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CCC24EBA0A13E34D00A6D3E3 /* FMDatabase.h */; }; CCC24EC20A13E34D00A6D3E3 /* FMDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = CCC24EBB0A13E34D00A6D3E3 /* FMDatabase.m */; }; @@ -46,11 +53,16 @@ 32A70AAB03705E1F00C91783 /* fmdb_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fmdb_Prefix.pch; sourceTree = ""; }; 8DD76FA10486AA7600D96B5E /* fmdb */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = fmdb; sourceTree = BUILT_PRODUCTS_DIR; }; C6859EA3029092ED04C91782 /* fmdb.1 */ = {isa = PBXFileReference; lastKnownFileType = text.man; path = fmdb.1; sourceTree = ""; }; + CC47A00D148581E9002CCDAB /* FMDatabaseQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FMDatabaseQueue.h; path = src/FMDatabaseQueue.h; sourceTree = ""; }; + CC47A00E148581E9002CCDAB /* FMDatabaseQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FMDatabaseQueue.m; path = src/FMDatabaseQueue.m; sourceTree = ""; }; CC50F2CB0DF9183600E4AAAE /* FMDatabaseAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FMDatabaseAdditions.m; path = src/FMDatabaseAdditions.m; sourceTree = ""; }; CC50F2CC0DF9183600E4AAAE /* FMDatabaseAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FMDatabaseAdditions.h; path = src/FMDatabaseAdditions.h; sourceTree = ""; }; CC8C138A0E3135C400FBE1E7 /* CHANGES_AND_TODO_LIST.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CHANGES_AND_TODO_LIST.txt; sourceTree = ""; }; CC8C138B0E3135C400FBE1E7 /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; CC8C138C0E3135C400FBE1E7 /* CONTRIBUTORS.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CONTRIBUTORS.txt; sourceTree = ""; }; + CC9E4EB713B31188005F9210 /* FMDatabasePool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FMDatabasePool.h; path = src/FMDatabasePool.h; sourceTree = ""; }; + CC9E4EB813B31188005F9210 /* FMDatabasePool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FMDatabasePool.m; path = src/FMDatabasePool.m; sourceTree = ""; }; + CCBE26C013B3BA8C006F6C37 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; CCBEBDAB0DF5DE1A003DDD08 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = /usr/lib/libsqlite3.dylib; sourceTree = ""; }; CCC24EBA0A13E34D00A6D3E3 /* FMDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FMDatabase.h; path = src/FMDatabase.h; sourceTree = ""; }; CCC24EBB0A13E34D00A6D3E3 /* FMDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FMDatabase.m; path = src/FMDatabase.m; sourceTree = ""; }; @@ -69,6 +81,7 @@ files = ( 8DD76F9C0486AA7600D96B5E /* Foundation.framework in Frameworks */, CCBEBDAC0DF5DE1A003DDD08 /* libsqlite3.dylib in Frameworks */, + CCBE26C113B3BA8C006F6C37 /* AppKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -103,12 +116,16 @@ 08FB7795FE84155DC02AAC07 /* Source */ = { isa = PBXGroup; children = ( + CC47A00D148581E9002CCDAB /* FMDatabaseQueue.h */, + CC47A00E148581E9002CCDAB /* FMDatabaseQueue.m */, CC50F2CC0DF9183600E4AAAE /* FMDatabaseAdditions.h */, CC50F2CB0DF9183600E4AAAE /* FMDatabaseAdditions.m */, CCC24EBA0A13E34D00A6D3E3 /* FMDatabase.h */, CCC24EBB0A13E34D00A6D3E3 /* FMDatabase.m */, CCC24EBF0A13E34D00A6D3E3 /* FMResultSet.h */, CCC24EC00A13E34D00A6D3E3 /* FMResultSet.m */, + CC9E4EB713B31188005F9210 /* FMDatabasePool.h */, + CC9E4EB813B31188005F9210 /* FMDatabasePool.m */, 32A70AAB03705E1F00C91783 /* fmdb_Prefix.pch */, CCC24EBE0A13E34D00A6D3E3 /* fmdb.m */, ); @@ -120,6 +137,7 @@ children = ( CCBEBDAB0DF5DE1A003DDD08 /* libsqlite3.dylib */, 08FB779EFE84155DC02AAC07 /* Foundation.framework */, + CCBE26C013B3BA8C006F6C37 /* AppKit.framework */, ); name = "External Frameworks and Libraries"; sourceTree = ""; @@ -151,6 +169,8 @@ EE42910712B42FC90088BD94 /* FMDatabase.h in Headers */, EE42910612B42FC30088BD94 /* FMDatabaseAdditions.h in Headers */, EE42910912B42FD00088BD94 /* FMResultSet.h in Headers */, + CC9E4EBA13B31188005F9210 /* FMDatabasePool.h in Headers */, + CC47A00F148581E9002CCDAB /* FMDatabaseQueue.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -197,8 +217,11 @@ /* Begin PBXProject section */ 08FB7793FE84155DC02AAC07 /* Project object */ = { isa = PBXProject; + attributes = { + LastUpgradeCheck = 0430; + }; buildConfigurationList = 1DEB927808733DD40010E9CD /* Build configuration list for PBXProject "fmdb" */; - compatibilityVersion = "Xcode 3.1"; + compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 1; knownRegions = ( @@ -226,6 +249,8 @@ CCC24EC50A13E34D00A6D3E3 /* fmdb.m in Sources */, CCC24EC70A13E34D00A6D3E3 /* FMResultSet.m in Sources */, CC50F2CD0DF9183600E4AAAE /* FMDatabaseAdditions.m in Sources */, + CC9E4EB913B31188005F9210 /* FMDatabasePool.m in Sources */, + CC47A010148581E9002CCDAB /* FMDatabaseQueue.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -236,6 +261,8 @@ EE42910812B42FCC0088BD94 /* FMDatabase.m in Sources */, EE42910512B42FBC0088BD94 /* FMDatabaseAdditions.m in Sources */, EE42910A12B42FD20088BD94 /* FMResultSet.m in Sources */, + CC9E4EBB13B31188005F9210 /* FMDatabasePool.m in Sources */, + CC47A011148581E9002CCDAB /* FMDatabaseQueue.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -245,9 +272,11 @@ 1DEB927508733DD40010E9CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_OBJCPP_ARC_ABI = YES; COPY_PHASE_STRIP = NO; GCC_DYNAMIC_NO_PIC = NO; - GCC_ENABLE_FIX_AND_CONTINUE = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = fmdb_Prefix.pch; @@ -261,6 +290,8 @@ isa = XCBuildConfiguration; buildSettings = { ARCHS = "$(ARCHS_STANDARD_32_64_BIT)"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_OBJCPP_ARC_ABI = YES; GCC_GENERATE_DEBUGGING_SYMBOLS = NO; GCC_MODEL_TUNING = G5; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -283,7 +314,6 @@ GCC_WARN_PEDANTIC = YES; GCC_WARN_SIGN_COMPARE = YES; GCC_WARN_UNUSED_VARIABLE = YES; - PREBINDING = NO; }; name = Debug; }; @@ -294,7 +324,6 @@ GCC_VERSION = com.apple.compilers.llvm.clang.1_0; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; - PREBINDING = NO; }; name = Release; }; @@ -305,7 +334,6 @@ COPY_PHASE_STRIP = NO; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; - PREBINDING = NO; PRODUCT_NAME = FMDB; }; name = Debug; @@ -316,8 +344,6 @@ ALWAYS_SEARCH_USER_PATHS = NO; COPY_PHASE_STRIP = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - GCC_ENABLE_FIX_AND_CONTINUE = NO; - PREBINDING = NO; PRODUCT_NAME = FMDB; ZERO_LINK = NO; }; diff --git a/src/FMDatabase.h b/src/FMDatabase.h index 587a01af..60d1a779 100644 --- a/src/FMDatabase.h +++ b/src/FMDatabase.h @@ -1,25 +1,66 @@ #import #import "sqlite3.h" #import "FMResultSet.h" +#import "FMDatabasePool.h" +/* +#ifndef MAC_OS_X_VERSION_10_7 + #define MAC_OS_X_VERSION_10_7 1070 +#endif + +#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_7 + #define FMDB_USE_WEAK_POOL 1 +#endif +*/ +#if ! __has_feature(objc_arc) + #define FMDBAutorelease(__v) ([__v autorelease]); + #define FMDBReturnAutoreleased FMDBAutorelease + + #define FMDBRetain(__v) ([__v retain]); + #define FMDBReturnRetained FMDBRetain + + #define FMDBRelease(__v) ([__v release]); +#else + // -fobjc-arc + #define FMDBAutorelease(__v) + #define FMDBReturnAutoreleased(__v) (__v) + + #define FMDBRetain(__v) + #define FMDBReturnRetained(__v) (__v) + + #define FMDBRelease(__v) +#endif + -@interface FMDatabase : NSObject -{ - sqlite3* db; - NSString* databasePath; - BOOL logsErrors; - BOOL crashOnErrors; - BOOL inUse; - BOOL inTransaction; - BOOL traceExecution; - BOOL checkedOut; - int busyRetryTimeout; - BOOL shouldCacheStatements; - NSMutableDictionary *cachedStatements; - NSMutableSet *openResultSets; +@interface FMDatabase : NSObject { + + sqlite3* _db; + NSString* _databasePath; + BOOL _logsErrors; + BOOL _crashOnErrors; + BOOL _traceExecution; + BOOL _checkedOut; + BOOL _shouldCacheStatements; + BOOL _isExecutingStatement; + BOOL _inTransaction; + int _busyRetryTimeout; + + NSMutableDictionary *_cachedStatements; + NSMutableSet *_openResultSets; + NSMutableSet *_openFunctions; + /* +#ifdef FMDB_USE_WEAK_POOL + __weak FMDatabasePool *_poolAccessViaMethodOnly; +#else + FMDatabasePool *_poolAccessViaMethodOnly; +#endif + + NSInteger _poolPopCount; + + FMDatabasePool *pool; + */ } -@property (assign) BOOL inTransaction; @property (assign) BOOL traceExecution; @property (assign) BOOL checkedOut; @property (assign) int busyRetryTimeout; @@ -39,56 +80,63 @@ - (BOOL)goodConnection; - (void)clearCachedStatements; - (void)closeOpenResultSets; +- (BOOL)hasOpenResultSets; // encryption methods. You need to have purchased the sqlite encryption extensions for these to work. - (BOOL)setKey:(NSString*)key; - (BOOL)rekey:(NSString*)key; - - (NSString *)databasePath; - (NSString*)lastErrorMessage; - (int)lastErrorCode; - (BOOL)hadError; +- (NSError*)lastError; + - (sqlite_int64)lastInsertRowId; - (sqlite3*)sqliteHandle; -- (BOOL)update:(NSString*)sql error:(NSError**)outErr bind:(id)bindArgs, ...; +- (BOOL)update:(NSString*)sql withErrorAndBindings:(NSError**)outErr, ...; - (BOOL)executeUpdate:(NSString*)sql, ...; - (BOOL)executeUpdateWithFormat:(NSString *)format, ...; - (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments; -- (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args; // you shouldn't ever need to call this. use the previous two instead. +- (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments; - (FMResultSet *)executeQuery:(NSString*)sql, ...; - (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...; - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments; -- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args; // you shouldn't ever need to call this. use the previous two instead. +- (FMResultSet *)executeQuery:(NSString *)sql withParameterDictionary:(NSDictionary *)arguments; - (BOOL)rollback; - (BOOL)commit; - (BOOL)beginTransaction; - (BOOL)beginDeferredTransaction; - -- (BOOL)inUse; -- (void)setInUse:(BOOL)value; - - +- (BOOL)inTransaction; - (BOOL)shouldCacheStatements; - (void)setShouldCacheStatements:(BOOL)value; -+ (BOOL)isThreadSafe; +#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; - (int)changes; +- (void)makeFunctionNamed:(NSString*)name maximumArguments:(int)count withBlock:(void (^)(sqlite3_context *context, int argc, sqlite3_value **argv))block; + @end @interface FMStatement : NSObject { - sqlite3_stmt *statement; - NSString *query; - long useCount; + sqlite3_stmt *_statement; + NSString *_query; + long _useCount; } @property (assign) long useCount; diff --git a/src/FMDatabase.m b/src/FMDatabase.m index aeb2f731..063a926a 100644 --- a/src/FMDatabase.m +++ b/src/FMDatabase.m @@ -1,29 +1,47 @@ #import "FMDatabase.h" #import "unistd.h" +#import + +@interface FMDatabase () + +- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args; +- (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args; +@end @implementation FMDatabase -@synthesize inTransaction; -@synthesize cachedStatements; -@synthesize logsErrors; -@synthesize crashOnErrors; -@synthesize busyRetryTimeout; -@synthesize checkedOut; -@synthesize traceExecution; +@synthesize cachedStatements=_cachedStatements; +@synthesize logsErrors=_logsErrors; +@synthesize crashOnErrors=_crashOnErrors; +@synthesize busyRetryTimeout=_busyRetryTimeout; +@synthesize checkedOut=_checkedOut; +@synthesize traceExecution=_traceExecution; + (id)databaseWithPath:(NSString*)aPath { - return [[[self alloc] initWithPath:aPath] autorelease]; + return FMDBReturnAutoreleased([[self alloc] initWithPath:aPath]); +} + ++ (NSString*)sqliteLibVersion { + return [NSString stringWithFormat:@"%s", sqlite3_libversion()]; +} + ++ (BOOL)isSQLiteThreadSafe { + // make sure to read the sqlite headers on this guy! + return sqlite3_threadsafe(); } - (id)initWithPath:(NSString*)aPath { + + assert(sqlite3_threadsafe()); // whoa there big boy- gotta make sure sqlite it happy with what we're going to do. + self = [super init]; if (self) { - databasePath = [aPath copy]; - openResultSets = [[NSMutableSet alloc] init]; - db = 0x00; - logsErrors = 0x00; - crashOnErrors = 0x00; - busyRetryTimeout = 0x00; + _databasePath = [aPath copy]; + _openResultSets = [[NSMutableSet alloc] init]; + _db = 0x00; + _logsErrors = 0x00; + _crashOnErrors = 0x00; + _busyRetryTimeout = 0x00; } return self; @@ -36,32 +54,30 @@ - (void)finalize { - (void)dealloc { [self close]; + FMDBRelease(_openResultSets); + FMDBRelease(_cachedStatements); + FMDBRelease(_databasePath); + FMDBRelease(_openFunctions); - [openResultSets release]; - [cachedStatements release]; - [databasePath release]; - +#if ! __has_feature(objc_arc) [super dealloc]; -} - -+ (NSString*)sqliteLibVersion { - return [NSString stringWithFormat:@"%s", sqlite3_libversion()]; +#endif } - (NSString *)databasePath { - return databasePath; + return _databasePath; } - (sqlite3*)sqliteHandle { - return db; + return _db; } - (BOOL)open { - if (db) { + if (_db) { return YES; } - int err = sqlite3_open((databasePath ? [databasePath fileSystemRepresentation] : ":memory:"), &db ); + int err = sqlite3_open((_databasePath ? [_databasePath fileSystemRepresentation] : ":memory:"), &_db ); if(err != SQLITE_OK) { NSLog(@"error opening!: %d", err); return NO; @@ -72,7 +88,7 @@ - (BOOL)open { #if SQLITE_VERSION_NUMBER >= 3005000 - (BOOL)openWithFlags:(int)flags { - int err = sqlite3_open_v2((databasePath ? [databasePath fileSystemRepresentation] : ":memory:"), &db, flags, NULL /* Name of VFS module to use */); + int err = sqlite3_open_v2((_databasePath ? [_databasePath fileSystemRepresentation] : ":memory:"), &_db, flags, NULL /* Name of VFS module to use */); if(err != SQLITE_OK) { NSLog(@"error opening!: %d", err); return NO; @@ -87,7 +103,7 @@ - (BOOL)close { [self clearCachedStatements]; [self closeOpenResultSets]; - if (!db) { + if (!_db) { return YES; } @@ -98,11 +114,14 @@ - (BOOL)close { do { retry = NO; - rc = sqlite3_close(db); + rc = sqlite3_close(_db); + if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { + retry = YES; usleep(20); - if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) { + + if (_busyRetryTimeout && (numberOfRetries++ > _busyRetryTimeout)) { NSLog(@"%s:%d", __FUNCTION__, __LINE__); NSLog(@"Database busy, unable to close"); return NO; @@ -111,7 +130,7 @@ - (BOOL)close { if (!triedFinalizingOpenStatements) { triedFinalizingOpenStatements = YES; sqlite3_stmt *pStmt; - while ((pStmt = sqlite3_next_stmt(db, 0x00)) !=0) { + while ((pStmt = sqlite3_next_stmt(_db, 0x00)) !=0) { NSLog(@"Closing leaked statement"); sqlite3_finalize(pStmt); } @@ -123,52 +142,58 @@ - (BOOL)close { } while (retry); - db = nil; + _db = nil; return YES; } - (void)clearCachedStatements { - NSEnumerator *e = [cachedStatements objectEnumerator]; - FMStatement *cachedStmt; - - while ((cachedStmt = [e nextObject])) { + for (FMStatement *cachedStmt in [_cachedStatements objectEnumerator]) { + //NSLog(@"cachedStmt: '%@'", cachedStmt); [cachedStmt close]; } - [cachedStatements removeAllObjects]; + [_cachedStatements removeAllObjects]; +} + +- (BOOL)hasOpenResultSets { + return [_openResultSets count] > 0; } - (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)]) { - [rs close]; - } + //Copy the set so we don't get mutation errors + NSMutableSet *openSetCopy = FMDBReturnAutoreleased([_openResultSets copy]); + for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) { + 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]; + + [_openResultSets removeObject:setValue]; } - (FMStatement*)cachedStatementForQuery:(NSString*)query { - return [cachedStatements objectForKey:query]; + return [_cachedStatements objectForKey: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]; - [query release]; + + [_cachedStatements setObject:statement forKey:query]; + + FMDBRelease(query); } @@ -178,7 +203,7 @@ - (BOOL)rekey:(NSString*)key { return NO; } - int rc = sqlite3_rekey(db, [key UTF8String], (int)strlen([key UTF8String])); + int rc = sqlite3_rekey(_db, [key UTF8String], (int)strlen([key UTF8String])); if (rc != SQLITE_OK) { NSLog(@"error on rekey: %d", rc); @@ -197,7 +222,7 @@ - (BOOL)setKey:(NSString*)key { return NO; } - int rc = sqlite3_key(db, [key UTF8String], (int)strlen([key UTF8String])); + int rc = sqlite3_key(_db, [key UTF8String], (int)strlen([key UTF8String])); return (rc == SQLITE_OK); #else @@ -207,7 +232,7 @@ - (BOOL)setKey:(NSString*)key { - (BOOL)goodConnection { - if (!db) { + if (!_db) { return NO; } @@ -225,7 +250,8 @@ - (void)warnInUse { NSLog(@"The FMDatabase %@ is currently in use.", self); #ifndef NS_BLOCK_ASSERTIONS - if (crashOnErrors) { + if (_crashOnErrors) { + abort(); NSAssert1(false, @"The FMDatabase %@ is currently in use.", self); } #endif @@ -233,12 +259,13 @@ - (void)warnInUse { - (BOOL)databaseExists { - if (!db) { + if (!_db) { NSLog(@"The FMDatabase %@ is not open.", self); #ifndef NS_BLOCK_ASSERTIONS - if (crashOnErrors) { + if (_crashOnErrors) { + abort(); NSAssert1(false, @"The FMDatabase %@ is not open.", self); } #endif @@ -250,7 +277,7 @@ - (BOOL)databaseExists { } - (NSString*)lastErrorMessage { - return [NSString stringWithUTF8String:sqlite3_errmsg(db)]; + return [NSString stringWithUTF8String:sqlite3_errmsg(_db)]; } - (BOOL)hadError { @@ -260,33 +287,40 @@ - (BOOL)hadError { } - (int)lastErrorCode { - return sqlite3_errcode(db); + 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; } - [self setInUse:YES]; - sqlite_int64 ret = sqlite3_last_insert_rowid(db); + _isExecutingStatement = YES; + + sqlite_int64 ret = sqlite3_last_insert_rowid(_db); - [self setInUse:NO]; + _isExecutingStatement = NO; return ret; } - (int)changes { - if (inUse) { + if (_isExecutingStatement) { [self warnInUse]; return 0; } - [self setInUse:YES]; - int ret = sqlite3_changes(db); - [self setInUse:NO]; + _isExecutingStatement = YES; + + int ret = sqlite3_changes(_db); + + _isExecutingStatement = NO; return ret; } @@ -333,7 +367,7 @@ - (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt { } } -- (void)_extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments { +- (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments { NSUInteger length = [sql length]; unichar last = '\0'; @@ -346,6 +380,7 @@ - (void)_extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMu case '@': arg = va_arg(args, id); break; case 'c': + // warning: second argument to 'va_arg' is of promotable type 'char'; this va_arg has undefined behavior because arguments will be promoted to 'int' arg = [NSString stringWithFormat:@"%c", va_arg(args, int)]; break; case 's': arg = [NSString stringWithUTF8String:va_arg(args, char*)]; break; @@ -359,10 +394,12 @@ - (void)_extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMu case 'h': i++; if (i < length && [sql characterAtIndex:i] == 'i') { - arg = [NSNumber numberWithInt:va_arg(args, int)]; + // warning: second argument to 'va_arg' is of promotable type 'short'; this va_arg has undefined behavior because arguments will be promoted to 'int' + arg = [NSNumber numberWithShort:va_arg(args, int)]; } else if (i < length && [sql characterAtIndex:i] == 'u') { - arg = [NSNumber numberWithInt:va_arg(args, int)]; + // warning: second argument to 'va_arg' is of promotable type 'unsigned short'; this va_arg has undefined behavior because arguments will be promoted to 'int' + arg = [NSNumber numberWithUnsignedShort:va_arg(args, uint)]; } else { i--; @@ -383,7 +420,8 @@ - (void)_extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMu case 'f': arg = [NSNumber numberWithDouble:va_arg(args, double)]; break; case 'g': - arg = [NSNumber numberWithDouble:va_arg(args, double)]; break; + // warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double' + arg = [NSNumber numberWithFloat:va_arg(args, double)]; break; case 'l': i++; if (i < length) { @@ -437,33 +475,35 @@ - (void)_extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMu } last = current; } - } -- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args { +- (FMResultSet *)executeQuery:(NSString *)sql withParameterDictionary:(NSDictionary *)arguments { + return [self executeQuery:sql withArgumentsInArray:nil orDictionary:arguments orVAList:nil]; +} + +- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args { if (![self databaseExists]) { return 0x00; } - if (inUse) { + if (_isExecutingStatement) { [self warnInUse]; return 0x00; } - [self setInUse:YES]; - - FMResultSet *rs = nil; + _isExecutingStatement = YES; int rc = 0x00; sqlite3_stmt *pStmt = 0x00; FMStatement *statement = 0x00; + FMResultSet *rs = 0x00; - if (traceExecution && sql) { + if (_traceExecution && sql) { NSLog(@"%@ executeQuery: %@", self, sql); } - if (shouldCacheStatements) { + if (_shouldCacheStatements) { statement = [self cachedStatementForQuery:sql]; pStmt = statement ? [statement statement] : 0x00; } @@ -474,36 +514,36 @@ - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arr if (!pStmt) { do { retry = NO; - rc = sqlite3_prepare_v2(db, [sql UTF8String], -1, &pStmt, 0); + rc = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0); if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { retry = YES; usleep(20); - if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) { + if (_busyRetryTimeout && (numberOfRetries++ > _busyRetryTimeout)) { NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]); NSLog(@"Database busy"); sqlite3_finalize(pStmt); - [self setInUse:NO]; + _isExecutingStatement = NO; return nil; } } else if (SQLITE_OK != rc) { - - if (logsErrors) { + if (_logsErrors) { NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); NSLog(@"DB Query: %@", sql); + NSLog(@"DB Path: %@", _databasePath); #ifndef NS_BLOCK_ASSERTIONS - if (crashOnErrors) { + if (_crashOnErrors) { + abort(); NSAssert2(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); } #endif } sqlite3_finalize(pStmt); - - [self setInUse:NO]; + _isExecutingStatement = NO; return nil; } } @@ -514,38 +554,66 @@ - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arr int idx = 0; int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!) - while (idx < queryCount) { + // If dictionaryArgs is passed in, that means we are using sqlite's named parameter support + if (dictionaryArgs) { - if (arrayArgs) { - obj = [arrayArgs objectAtIndex:idx]; - } - else { - obj = va_arg(args, id); + for (NSString *dictionaryKey in [dictionaryArgs allKeys]) { + + // Prefix the key with a colon. + NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey]; + + // Get the index for the parameter name. + int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]); + + FMDBRelease(parameterName); + + if (namedIdx > 0) { + // Standard binding from here. + [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt]; + } + else { + NSLog(@"Could not find index for %@", dictionaryKey); + } } - if (traceExecution) { - NSLog(@"obj: %@", obj); + // we need the count of params to avoid an error below. + idx = (int) [[dictionaryArgs allKeys] count]; + } + else { + + while (idx < queryCount) { + + if (arrayArgs) { + obj = [arrayArgs objectAtIndex:idx]; + } + else { + obj = va_arg(args, id); + } + + if (_traceExecution) { + NSLog(@"obj: %@", obj); + } + + idx++; + + [self bindObject:obj toColumn:idx inStatement:pStmt]; } - - idx++; - - [self bindObject:obj toColumn:idx inStatement:pStmt]; } if (idx != queryCount) { NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)"); sqlite3_finalize(pStmt); - [self setInUse:NO]; + _isExecutingStatement = NO; return nil; } - [statement retain]; // to balance the release below + FMDBRetain(statement); // to balance the release below if (!statement) { statement = [[FMStatement alloc] init]; [statement setStatement:pStmt]; - if (shouldCacheStatements) { + if (_shouldCacheStatements) { [self setCachedStatement:statement forQuery:sql]; } } @@ -553,14 +621,15 @@ - (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]; + [_openResultSets addObject:openResultSet]; - statement.useCount = statement.useCount + 1; + [statement setUseCount:[statement useCount] + 1]; - [statement release]; + FMDBRelease(statement); - [self setInUse:NO]; + _isExecutingStatement = NO; return rs; } @@ -569,7 +638,7 @@ - (FMResultSet *)executeQuery:(NSString*)sql, ... { va_list args; va_start(args, sql); - id result = [self executeQuery:sql withArgumentsInArray:nil orVAList:args]; + id result = [self executeQuery:sql withArgumentsInArray:nil orDictionary:nil orVAList:args]; va_end(args); return result; @@ -581,7 +650,7 @@ - (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... { NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]]; NSMutableArray *arguments = [NSMutableArray array]; - [self _extractSQL:format argumentsList:args intoString:sql arguments:arguments]; + [self extractSQL:format argumentsList:args intoString:sql arguments:arguments]; va_end(args); @@ -589,31 +658,31 @@ - (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... { } - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments { - return [self executeQuery:sql withArgumentsInArray:arguments orVAList:nil]; + return [self executeQuery:sql withArgumentsInArray:arguments orDictionary:nil orVAList:nil]; } -- (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args { +- (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args { if (![self databaseExists]) { return NO; } - if (inUse) { + if (_isExecutingStatement) { [self warnInUse]; return NO; } - [self setInUse:YES]; + _isExecutingStatement = YES; int rc = 0x00; sqlite3_stmt *pStmt = 0x00; FMStatement *cachedStmt = 0x00; - if (traceExecution && sql) { + if (_traceExecution && sql) { NSLog(@"%@ executeUpdate: %@", self, sql); } - if (shouldCacheStatements) { + if (_shouldCacheStatements) { cachedStmt = [self cachedStatementForQuery:sql]; pStmt = cachedStmt ? [cachedStmt statement] : 0x00; } @@ -625,73 +694,101 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra do { retry = NO; - rc = sqlite3_prepare_v2(db, [sql UTF8String], -1, &pStmt, 0); + rc = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0); if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { retry = YES; usleep(20); - if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) { + if (_busyRetryTimeout && (numberOfRetries++ > _busyRetryTimeout)) { NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]); NSLog(@"Database busy"); sqlite3_finalize(pStmt); - [self setInUse:NO]; + _isExecutingStatement = NO; return NO; } } else if (SQLITE_OK != rc) { - - if (logsErrors) { + if (_logsErrors) { NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); NSLog(@"DB Query: %@", sql); + NSLog(@"DB Path: %@", _databasePath); #ifndef NS_BLOCK_ASSERTIONS - if (crashOnErrors) { + if (_crashOnErrors) { + abort(); NSAssert2(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); } #endif } sqlite3_finalize(pStmt); - [self setInUse:NO]; if (outErr) { - *outErr = [NSError errorWithDomain:[NSString stringWithUTF8String:sqlite3_errmsg(db)] code:rc userInfo:nil]; + *outErr = [NSError errorWithDomain:[NSString stringWithUTF8String:sqlite3_errmsg(_db)] code:rc userInfo:nil]; } + _isExecutingStatement = NO; return NO; } } while (retry); } - id obj; int idx = 0; int queryCount = sqlite3_bind_parameter_count(pStmt); - while (idx < queryCount) { + // If dictionaryArgs is passed in, that means we are using sqlite's named parameter support + if (dictionaryArgs) { - if (arrayArgs) { - obj = [arrayArgs objectAtIndex:idx]; - } - else { - obj = va_arg(args, id); + for (NSString *dictionaryKey in [dictionaryArgs allKeys]) { + + // Prefix the key with a colon. + NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey]; + + // Get the index for the parameter name. + int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]); + + FMDBRelease(parameterName); + + if (namedIdx > 0) { + // Standard binding from here. + [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt]; + } + else { + NSLog(@"Could not find index for %@", dictionaryKey); + } } + // we need the count of params to avoid an error below. + idx = (int) [[dictionaryArgs allKeys] count]; + } + else { - if (traceExecution) { - NSLog(@"obj: %@", obj); + while (idx < queryCount) { + + if (arrayArgs) { + obj = [arrayArgs objectAtIndex:idx]; + } + else { + obj = va_arg(args, id); + } + + if (_traceExecution) { + NSLog(@"obj: %@", obj); + } + + idx++; + + [self bindObject:obj toColumn:idx inStatement:pStmt]; } - - idx++; - - [self bindObject:obj toColumn:idx inStatement:pStmt]; } + 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; return NO; } @@ -699,6 +796,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; @@ -715,59 +813,66 @@ - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArra } usleep(20); - if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) { + if (_busyRetryTimeout && (numberOfRetries++ > _busyRetryTimeout)) { NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]); NSLog(@"Database busy"); retry = NO; } } - else if (SQLITE_DONE == rc || SQLITE_ROW == rc) { + else if (SQLITE_DONE == rc) { // all is well, let's return. } else if (SQLITE_ERROR == rc) { - NSLog(@"Error calling sqlite3_step (%d: %s) SQLITE_ERROR", rc, sqlite3_errmsg(db)); + NSLog(@"Error calling sqlite3_step (%d: %s) SQLITE_ERROR", rc, sqlite3_errmsg(_db)); NSLog(@"DB Query: %@", sql); } else if (SQLITE_MISUSE == rc) { // uh oh. - NSLog(@"Error calling sqlite3_step (%d: %s) SQLITE_MISUSE", rc, sqlite3_errmsg(db)); + NSLog(@"Error calling sqlite3_step (%d: %s) SQLITE_MISUSE", rc, sqlite3_errmsg(_db)); NSLog(@"DB Query: %@", sql); } else { // wtf? - NSLog(@"Unknown error calling sqlite3_step (%d: %s) eu", rc, sqlite3_errmsg(db)); + NSLog(@"Unknown error calling sqlite3_step (%d: %s) eu", rc, sqlite3_errmsg(_db)); NSLog(@"DB Query: %@", sql); } } while (retry); - assert( rc!=SQLITE_ROW ); - + if (rc == SQLITE_ROW) { + NSAssert1(NO, @"A executeUpdate is being called with a query string '%@'", sql); + } - if (shouldCacheStatements && !cachedStmt) { + if (_shouldCacheStatements && !cachedStmt) { cachedStmt = [[FMStatement alloc] init]; [cachedStmt setStatement:pStmt]; [self setCachedStatement:cachedStmt forQuery:sql]; - [cachedStmt release]; + FMDBRelease(cachedStmt); } + int closeErrorCode; + if (cachedStmt) { - cachedStmt.useCount = cachedStmt.useCount + 1; - rc = sqlite3_reset(pStmt); + [cachedStmt setUseCount:[cachedStmt useCount] + 1]; + closeErrorCode = sqlite3_reset(pStmt); } else { /* Finalize the virtual machine. This releases all memory and other ** resources allocated by the sqlite3_prepare() call above. */ - rc = sqlite3_finalize(pStmt); + closeErrorCode = sqlite3_finalize(pStmt); } - [self setInUse:NO]; + if (closeErrorCode != SQLITE_OK) { + NSLog(@"Unknown error finalizing or resetting statement (%d: %s)", closeErrorCode, sqlite3_errmsg(_db)); + NSLog(@"DB Query: %@", sql); + } - return (rc == SQLITE_OK); + _isExecutingStatement = NO; + return (rc == SQLITE_DONE || rc == SQLITE_OK); } @@ -775,104 +880,206 @@ - (BOOL)executeUpdate:(NSString*)sql, ... { va_list args; va_start(args, sql); - BOOL result = [self executeUpdate:sql error:nil withArgumentsInArray:nil orVAList:args]; + BOOL result = [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:nil orVAList:args]; va_end(args); return result; } - - - (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments { - return [self executeUpdate:sql error:nil withArgumentsInArray:arguments orVAList:nil]; + return [self executeUpdate:sql error:nil withArgumentsInArray:arguments orDictionary:nil orVAList:nil]; +} + +- (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments { + return [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:arguments orVAList:nil]; } - (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]; + + [self extractSQL:format argumentsList:args intoString:sql arguments:arguments]; va_end(args); return [self executeUpdate:sql withArgumentsInArray:arguments]; } -- (BOOL)update:(NSString*)sql error:(NSError**)outErr bind:(id)bindArgs, ... { +- (BOOL)update:(NSString*)sql withErrorAndBindings:(NSError**)outErr, ... { va_list args; - va_start(args, bindArgs); + va_start(args, outErr); - BOOL result = [self executeUpdate:sql error:outErr withArgumentsInArray:nil orVAList:args]; + BOOL result = [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:args]; va_end(args); return result; } - (BOOL)rollback { - BOOL b = [self executeUpdate:@"ROLLBACK TRANSACTION;"]; + BOOL b = [self executeUpdate:@"rollback transaction"]; + if (b) { - inTransaction = NO; + _inTransaction = NO; } + return b; } - (BOOL)commit { - BOOL b = [self executeUpdate:@"COMMIT TRANSACTION;"]; + BOOL b = [self executeUpdate:@"commit transaction"]; + if (b) { - inTransaction = NO; + _inTransaction = NO; } + return b; } - (BOOL)beginDeferredTransaction { - BOOL b = [self executeUpdate:@"BEGIN DEFERRED TRANSACTION;"]; + + BOOL b = [self executeUpdate:@"begin deferred transaction"]; if (b) { - inTransaction = YES; + _inTransaction = YES; } + return b; } - (BOOL)beginTransaction { - BOOL b = [self executeUpdate:@"BEGIN EXCLUSIVE TRANSACTION;"]; + + BOOL b = [self executeUpdate:@"begin exclusive transaction"]; if (b) { - inTransaction = YES; + _inTransaction = YES; } + return b; } +- (BOOL)inTransaction { + return _inTransaction; +} + +#if SQLITE_VERSION_NUMBER >= 3007000 + +- (BOOL)startSavePointWithName:(NSString*)name error:(NSError**)outErr { + + // FIXME: make sure the savepoint name doesn't have a ' in it. + + NSParameterAssert(name); + + if (![self executeUpdate:[NSString stringWithFormat:@"savepoint '%@';", name]]) { + + if (*outErr) { + *outErr = [self lastError]; + } + + return NO; + } + + return YES; +} +- (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError**)outErr { + + NSParameterAssert(name); + + BOOL worked = [self executeUpdate:[NSString stringWithFormat:@"release savepoint '%@';", name]]; + + if (!worked && *outErr) { + *outErr = [self lastError]; + } + + return worked; +} -- (BOOL)inUse { - return inUse || inTransaction; +- (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError**)outErr { + + NSParameterAssert(name); + + BOOL worked = [self executeUpdate:[NSString stringWithFormat:@"rollback transaction to savepoint '%@';", name]]; + + if (!worked && *outErr) { + *outErr = [self lastError]; + } + + return worked; } -- (void)setInUse:(BOOL)b { - inUse = b; +- (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; + return _shouldCacheStatements; } - (void)setShouldCacheStatements:(BOOL)value { - shouldCacheStatements = value; + _shouldCacheStatements = value; - if (shouldCacheStatements && !cachedStatements) { + if (_shouldCacheStatements && !_cachedStatements) { [self setCachedStatements:[NSMutableDictionary dictionary]]; } - if (!shouldCacheStatements) { + if (!_shouldCacheStatements) { [self setCachedStatements:nil]; } } -+ (BOOL)isThreadSafe { - // make sure to read the sqlite headers on this guy! - return sqlite3_threadsafe(); +void FMDBBlockSQLiteCallBackFunction(sqlite3_context *context, int argc, sqlite3_value **argv); +void FMDBBlockSQLiteCallBackFunction(sqlite3_context *context, int argc, sqlite3_value **argv) { +#if ! __has_feature(objc_arc) + void (^block)(sqlite3_context *context, int argc, sqlite3_value **argv) = (id)sqlite3_user_data(context); +#else + void (^block)(sqlite3_context *context, int argc, sqlite3_value **argv) = (__bridge id)sqlite3_user_data(context); +#endif + block(context, argc, argv); +} + + +- (void)makeFunctionNamed:(NSString*)name maximumArguments:(int)count withBlock:(void (^)(sqlite3_context *context, int argc, sqlite3_value **argv))block { + + if (!_openFunctions) { + _openFunctions = [NSMutableSet new]; + } + + id b = FMDBReturnAutoreleased([block copy]); + + [_openFunctions addObject:b]; + + /* I tried adding custom functions to release the block when the connection is destroyed- but they seemed to never be called, so we use _openFunctions to store the values instead. */ +#if ! __has_feature(objc_arc) + sqlite3_create_function([self sqliteHandle], [name UTF8String], count, SQLITE_UTF8, (void*)b, &FMDBBlockSQLiteCallBackFunction, 0x00, 0x00); +#else + sqlite3_create_function([self sqliteHandle], [name UTF8String], count, SQLITE_UTF8, (__bridge void*)b, &FMDBBlockSQLiteCallBackFunction, 0x00, 0x00); +#endif } @end @@ -880,9 +1087,9 @@ + (BOOL)isThreadSafe { @implementation FMStatement -@synthesize statement; -@synthesize query; -@synthesize useCount; +@synthesize statement=_statement; +@synthesize query=_query; +@synthesize useCount=_useCount; - (void)finalize { [self close]; @@ -891,25 +1098,27 @@ - (void)finalize { - (void)dealloc { [self close]; - [query release]; + FMDBRelease(_query); +#if ! __has_feature(objc_arc) [super dealloc]; +#endif } - (void)close { - if (statement) { - sqlite3_finalize(statement); - statement = 0x00; + if (_statement) { + sqlite3_finalize(_statement); + _statement = 0x00; } } - (void)reset { - if (statement) { - sqlite3_reset(statement); + if (_statement) { + sqlite3_reset(_statement); } } - (NSString*)description { - return [NSString stringWithFormat:@"%@ %d hit(s) for query %@", [super description], useCount, query]; + return [NSString stringWithFormat:@"%@ %ld hit(s) for query %@", [super description], _useCount, _query]; } diff --git a/src/FMDatabaseAdditions.m b/src/FMDatabaseAdditions.m index c1c0c8e8..43383536 100644 --- a/src/FMDatabaseAdditions.m +++ b/src/FMDatabaseAdditions.m @@ -9,12 +9,16 @@ #import "FMDatabase.h" #import "FMDatabaseAdditions.h" +@interface FMDatabase (PrivateStuff) +- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args; +@end + @implementation FMDatabase (FMDatabaseAdditions) #define RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(type, sel) \ va_list args; \ va_start(args, query); \ -FMResultSet *resultSet = [self executeQuery:query withArgumentsInArray:0x00 orVAList:args]; \ +FMResultSet *resultSet = [self executeQuery:query withArgumentsInArray:0x00 orDictionary:0x00 orVAList:args]; \ va_end(args); \ if (![resultSet next]) { return (type)0; } \ type ret = [resultSet sel:0]; \ @@ -52,24 +56,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 +83,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 +95,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,7 +111,8 @@ - (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName { break; } } - //close and free object + + //If this is not done FMDatabase instance stays out of pool [rs close]; return returnBool; @@ -117,15 +124,14 @@ - (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); + int rc = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0); if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED) { keepTrying = YES; usleep(20); - if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) { + if (_busyRetryTimeout && (numberOfRetries++ > _busyRetryTimeout)) { NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]); NSLog(@"Database busy"); } @@ -141,7 +147,6 @@ - (BOOL)validateSQL:(NSString*)sql error:(NSError**)error { } } - [self setInUse:NO]; sqlite3_finalize(pStmt); return validationSucceeded; diff --git a/src/FMDatabasePool.h b/src/FMDatabasePool.h new file mode 100644 index 00000000..ea7366d9 --- /dev/null +++ b/src/FMDatabasePool.h @@ -0,0 +1,149 @@ +// +// FMDatabasePool.h +// fmdb +// +// Created by August Mueller on 6/22/11. +// Copyright 2011 Flying Meat Inc. All rights reserved. +// + +#import +#import "sqlite3.h" + +/* + + ***README OR SUFFER*** +Before using FMDatabasePool, please consider using FMDatabaseQueue instead. + +I'm also not 100% sold on this interface. So if you use FMDatabasePool, things like +[[pool db] popFromPool] might go away. In fact, I'm pretty darn sure they will. + +If you really really really know what you're doing and FMDatabasePool is what +you really really need, OK you can use it. But just be careful not to deadlock! + +First, make your pool. + + FMDatabasePool *pool = [FMDatabasePool databasePoolWithPath:aPath]; + +If you just have a single statement- use it like so: + + [[pool db] executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:42]]; + +The pool's db method will return an instance of FMDatabase that knows it is in a pool. After it is done with the update, it will place itself back into the pool. + +Making a query is similar: + + FMResultSet *rs = [[pool db] executeQuery:@"SELECT * FROM myTable"]; + while ([rs next]) { + //retrieve values for each record + } + +When the result set is exhausted or [rs close] is called, the result set will tell the database it was created from to put itself back into the pool for use later on. + +If you'd rather use multiple queries without having to call [pool db] each time, you can grab a database instance, tell it to stay out of the pool, and then tell it to go back in the pool when you're done: + + FMDatabase *db = [[pool db] popFromPool]; + … + [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]]; + … + // put the database back in the pool. + [db pushToPool]; + +Alternatively, you can use this nifty block based approach: + + [dbPool useDatabase: ^(FMDatabase *aDb) { + [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]]; + }]; + +And it will do the right thing. + +Starting a transaction will keep the db from going back into the pool automatically: + + FMDatabase *db = [pool db]; + [db beginTransaction]; + + [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]]; + + [db commit]; // or a rollback here would work as well. + + +There is also a block based transaction approach: + + [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]]; + }]; + + + +If you check out a database, but never execute a statement or query, **you need to put it back in the pool yourself**. + + FMDatabase *db = [pool db]; + // lala, don't do anything with the database + … + // oh look, I BETTER PUT THE DB BACK IN THE POOL OR ELSE IT IS GOING TO LEAK: + [db pushToPool]; + +*/ + + + + + + + + + +@class FMDatabase; + +@interface FMDatabasePool : NSObject { + NSString *_path; + + dispatch_queue_t _lockQueue; + + NSMutableArray *_databaseInPool; + NSMutableArray *_databaseOutPool; + + __unsafe_unretained id _delegate; + + NSUInteger _maximumNumberOfDatabasesToCreate; +} + +@property (retain) NSString *path; +@property (assign) id delegate; +@property (assign) NSUInteger maximumNumberOfDatabasesToCreate; + ++ (id)databasePoolWithPath:(NSString*)aPath; +- (id)initWithPath:(NSString*)aPath; + +- (NSUInteger)countOfCheckedInDatabases; +- (NSUInteger)countOfCheckedOutDatabases; +- (NSUInteger)countOfOpenDatabases; +- (void)releaseAllDatabases; + +- (void)inDatabase:(void (^)(FMDatabase *db))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 + + +@interface NSObject (FMDatabasePoolDelegate) + +- (BOOL)databasePool:(FMDatabasePool*)pool shouldAddDatabaseToPool:(FMDatabase*)database; + +@end + diff --git a/src/FMDatabasePool.m b/src/FMDatabasePool.m new file mode 100644 index 00000000..8cdc3379 --- /dev/null +++ b/src/FMDatabasePool.m @@ -0,0 +1,244 @@ +// +// FMDatabasePool.m +// fmdb +// +// Created by August Mueller on 6/22/11. +// Copyright 2011 Flying Meat Inc. All rights reserved. +// + +#import "FMDatabasePool.h" +#import "FMDatabase.h" + +@interface FMDatabasePool() + +- (void)pushDatabaseBackInPool:(FMDatabase*)db; +- (FMDatabase*)db; + +@end + + +@implementation FMDatabasePool +@synthesize path=_path; +@synthesize delegate=_delegate; +@synthesize maximumNumberOfDatabasesToCreate=_maximumNumberOfDatabasesToCreate; + + ++ (id)databasePoolWithPath:(NSString*)aPath { + return FMDBReturnAutoreleased([[self alloc] initWithPath:aPath]); +} + +- (id)initWithPath:(NSString*)aPath { + + self = [super init]; + + if (self != nil) { + _path = [aPath copy]; + _lockQueue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL); + _databaseInPool = FMDBReturnRetained([NSMutableArray array]); + _databaseOutPool = FMDBReturnRetained([NSMutableArray array]); + } + + return self; +} + +- (void)dealloc { + + _delegate = 0x00; + FMDBRelease(_path); + FMDBRelease(_databaseInPool); + FMDBRelease(_databaseOutPool); + + if (_lockQueue) { + dispatch_release(_lockQueue); + _lockQueue = 0x00; + } +#if ! __has_feature(objc_arc) + [super dealloc]; +#endif +} + + +- (void)executeLocked:(void (^)(void))aBlock { + dispatch_sync(_lockQueue, aBlock); +} + +- (void)pushDatabaseBackInPool:(FMDatabase*)db { + + if (!db) { // db can be null if we set an upper bound on the # of databases to create. + return; + } + + [self executeLocked:^() { + + if ([_databaseInPool containsObject:db]) { + [[NSException exceptionWithName:@"Database already in pool" reason:@"The FMDatabase being put back into the pool is already present in the pool" userInfo:nil] raise]; + } + + [_databaseInPool addObject:db]; + [_databaseOutPool removeObject:db]; + + }]; +} + +- (FMDatabase*)db { + + __block FMDatabase *db; + + [self executeLocked:^() { + db = [_databaseInPool lastObject]; + + if (db) { + [_databaseOutPool addObject:db]; + [_databaseInPool removeLastObject]; + } + else { + + if (_maximumNumberOfDatabasesToCreate) { + NSUInteger currentCount = [_databaseOutPool count] + [_databaseInPool count]; + + if (currentCount >= _maximumNumberOfDatabasesToCreate) { + NSLog(@"Maximum number of databases (%ld) has already been reached!", (long)currentCount); + return; + } + } + + db = [FMDatabase databaseWithPath:_path]; + } + + //This ensures that the db is opened before returning + if ([db open]) { + if ([_delegate respondsToSelector:@selector(databasePool:shouldAddDatabaseToPool:)] && ![_delegate databasePool:self shouldAddDatabaseToPool:db]) { + [db close]; + db = 0x00; + } + else { + //It should not get added in the pool twice if lastObject was found + if (![_databaseOutPool containsObject:db]) { + [_databaseOutPool addObject:db]; + } + } + } + else { + NSLog(@"Could not open up the database at path %@", _path); + db = 0x00; + } + }]; + + return db; +} + +- (NSUInteger)countOfCheckedInDatabases { + + __block NSInteger count; + + [self executeLocked:^() { + count = [_databaseInPool count]; + }]; + + return count; +} + +- (NSUInteger)countOfCheckedOutDatabases { + + __block NSInteger count; + + [self executeLocked:^() { + count = [_databaseOutPool count]; + }]; + + return count; +} + +- (NSUInteger)countOfOpenDatabases { + __block NSInteger count; + + [self executeLocked:^() { + count = [_databaseOutPool count] + [_databaseInPool count]; + }]; + + return count; +} + +- (void)releaseAllDatabases { + [self executeLocked:^() { + [_databaseOutPool removeAllObjects]; + [_databaseInPool removeAllObjects]; + }]; +} + +- (void)inDatabase:(void (^)(FMDatabase *db))block { + + FMDatabase *db = [self db]; + + block(db); + + [self pushDatabaseBackInPool:db]; +} + +- (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block { + + BOOL shouldRollback = NO; + + FMDatabase *db = [self db]; + + if (useDeferred) { + [db beginDeferredTransaction]; + } + else { + [db beginTransaction]; + } + + + block(db, &shouldRollback); + + if (shouldRollback) { + [db rollback]; + } + else { + [db commit]; + } + + [self pushDatabaseBackInPool:db]; +} + +- (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]; +} +#if SQLITE_VERSION_NUMBER >= 3007000 +- (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]) { + [self pushDatabaseBackInPool:db]; + return err; + } + + block(db, &shouldRollback); + + if (shouldRollback) { + [db rollbackToSavePointWithName:name error:&err]; + } + else { + [db releaseSavePointWithName:name error:&err]; + } + + [self pushDatabaseBackInPool:db]; + + return err; +} +#endif + +@end diff --git a/src/FMDatabaseQueue.h b/src/FMDatabaseQueue.h new file mode 100644 index 00000000..d80cb27d --- /dev/null +++ b/src/FMDatabaseQueue.h @@ -0,0 +1,38 @@ +// +// FMDatabasePool.h +// fmdb +// +// Created by August Mueller on 6/22/11. +// Copyright 2011 Flying Meat Inc. All rights reserved. +// + +#import +#import "sqlite3.h" + +@class FMDatabase; + +@interface FMDatabaseQueue : NSObject { + NSString *_path; + dispatch_queue_t _queue; + FMDatabase *_db; +} + +@property (retain) NSString *path; + ++ (id)databaseQueueWithPath:(NSString*)aPath; +- (id)initWithPath:(NSString*)aPath; +- (void)close; + +- (void)inDatabase:(void (^)(FMDatabase *db))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 + diff --git a/src/FMDatabaseQueue.m b/src/FMDatabaseQueue.m new file mode 100644 index 00000000..8067c1cf --- /dev/null +++ b/src/FMDatabaseQueue.m @@ -0,0 +1,176 @@ +// +// FMDatabasePool.m +// fmdb +// +// Created by August Mueller on 6/22/11. +// Copyright 2011 Flying Meat Inc. All rights reserved. +// + +#import "FMDatabaseQueue.h" +#import "FMDatabase.h" + +/* + + Note: we call [self retain]; before using dispatch_sync, just incase + FMDatabaseQueue is released on another thread and we're in the middle of doing + something in dispatch_sync + + */ + +@implementation FMDatabaseQueue + +@synthesize path = _path; + ++ (id)databaseQueueWithPath:(NSString*)aPath { + + FMDatabaseQueue *q = [[self alloc] initWithPath:aPath]; + + FMDBAutorelease(q); + + return q; +} + +- (id)initWithPath:(NSString*)aPath { + + self = [super init]; + + if (self != nil) { + + _db = [FMDatabase databaseWithPath:aPath]; + FMDBRetain(_db); + + if (![_db open]) { + NSLog(@"Could not create database queue for path %@", aPath); + FMDBRelease(self); + return 0x00; + } + + _path = FMDBReturnRetained(aPath); + + _queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL); + } + + return self; +} + +- (void)dealloc { + + FMDBRelease(_db); + FMDBRelease(_path); + + if (_queue) { + dispatch_release(_queue); + _queue = 0x00; + } +#if ! __has_feature(objc_arc) + [super dealloc]; +#endif +} + +- (void)close { + FMDBRetain(self); + dispatch_sync(_queue, ^() { + [_db close]; + FMDBRelease(_db); + _db = 0x00; + }); + FMDBRelease(self); +} + +- (FMDatabase*)database { + if (!_db) { + _db = FMDBReturnRetained([FMDatabase databaseWithPath:_path]); + + if (![_db open]) { + NSLog(@"FMDatabaseQueue could not reopen database for path %@", _path); + FMDBRelease(_db); + _db = 0x00; + return 0x00; + } + } + + return _db; +} + +- (void)inDatabase:(void (^)(FMDatabase *db))block { + FMDBRetain(self); + + dispatch_sync(_queue, ^() { + + FMDatabase *db = [self database]; + block(db); + + if ([db hasOpenResultSets]) { + NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]"); + } + }); + + FMDBRelease(self); +} + + +- (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block { + FMDBRetain(self); + dispatch_sync(_queue, ^() { + + BOOL shouldRollback = NO; + + if (useDeferred) { + [[self database] beginDeferredTransaction]; + } + else { + [[self database] beginTransaction]; + } + + block([self database], &shouldRollback); + + if (shouldRollback) { + [[self database] rollback]; + } + else { + [[self database] commit]; + } + }); + + FMDBRelease(self); +} + +- (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]; +} + +#if SQLITE_VERSION_NUMBER >= 3007000 +- (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block { + + static unsigned long savePointIdx = 0; + __block NSError *err = 0x00; + FMDBRetain(self); + dispatch_sync(_queue, ^() { + + NSString *name = [NSString stringWithFormat:@"savePoint%ld", savePointIdx++]; + + BOOL shouldRollback = NO; + + if ([[self database] startSavePointWithName:name error:&err]) { + + block([self database], &shouldRollback); + + if (shouldRollback) { + [[self database] rollbackToSavePointWithName:name error:&err]; + } + else { + [[self database] releaseSavePointWithName:name error:&err]; + } + + } + }); + FMDBRelease(self); + return err; +} +#endif + +@end diff --git a/src/FMResultSet.h b/src/FMResultSet.h index ccf1d7ba..2c9e40c8 100644 --- a/src/FMResultSet.h +++ b/src/FMResultSet.h @@ -17,12 +17,12 @@ @class FMStatement; @interface FMResultSet : NSObject { - FMDatabase *parentDB; - FMStatement *statement; + FMDatabase *_parentDB; + FMStatement *_statement; - NSString *query; - NSMutableDictionary *columnNameToIndexMap; - BOOL columnNamesSetup; + NSString *_query; + NSMutableDictionary *_columnNameToIndexMap; + BOOL _columnNamesSetup; } @property (retain) NSString *query; diff --git a/src/FMResultSet.m b/src/FMResultSet.m index 52f5dc57..6bc7b1ba 100644 --- a/src/FMResultSet.m +++ b/src/FMResultSet.m @@ -13,9 +13,9 @@ - (void)setColumnNameToIndexMap:(NSMutableDictionary *)value; @end @implementation FMResultSet -@synthesize query; -@synthesize columnNameToIndexMap; -@synthesize statement; +@synthesize query=_query; +@synthesize columnNameToIndexMap=_columnNameToIndexMap; +@synthesize statement=_statement; + (id)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB { @@ -24,7 +24,7 @@ + (id)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDat [rs setStatement:statement]; [rs setParentDB:aDB]; - return [rs autorelease]; + return FMDBReturnAutoreleased(rs); } - (void)finalize { @@ -35,83 +35,85 @@ - (void)finalize { - (void)dealloc { [self close]; - [query release]; - query = nil; + FMDBRelease(_query); + _query = nil; - [columnNameToIndexMap release]; - columnNameToIndexMap = nil; + FMDBRelease(_columnNameToIndexMap); + _columnNameToIndexMap = nil; +#if ! __has_feature(objc_arc) [super dealloc]; +#endif } - (void)close { - [statement reset]; - [statement release]; - statement = nil; + [_statement reset]; + FMDBRelease(_statement); + _statement = nil; // we don't need this anymore... (i think) - //[parentDB setInUse:NO]; - [parentDB resultSetDidClose:self]; - parentDB = nil; + //[_parentDB setInUse:NO]; + [_parentDB resultSetDidClose:self]; + _parentDB = nil; } - (int)columnCount { - return sqlite3_column_count(statement.statement); + return sqlite3_column_count([_statement statement]); } - (void)setupColumnNames { - if (!columnNameToIndexMap) { + if (!_columnNameToIndexMap) { [self setColumnNameToIndexMap:[NSMutableDictionary dictionary]]; } - int columnCount = sqlite3_column_count(statement.statement); + int columnCount = sqlite3_column_count([_statement statement]); int columnIdx = 0; for (columnIdx = 0; columnIdx < columnCount; columnIdx++) { - [columnNameToIndexMap setObject:[NSNumber numberWithInt:columnIdx] - forKey:[[NSString stringWithUTF8String:sqlite3_column_name(statement.statement, columnIdx)] lowercaseString]]; + [_columnNameToIndexMap setObject:[NSNumber numberWithInt:columnIdx] + forKey:[[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)] lowercaseString]]; } - columnNamesSetup = YES; + _columnNamesSetup = YES; } - (void)kvcMagic:(id)object { - int columnCount = sqlite3_column_count(statement.statement); + int columnCount = sqlite3_column_count([_statement statement]); int columnIdx = 0; for (columnIdx = 0; columnIdx < columnCount; columnIdx++) { - const char *c = (const char *)sqlite3_column_text(statement.statement, columnIdx); + const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx); // check for a null row if (c) { NSString *s = [NSString stringWithUTF8String:c]; - [object setValue:s forKey:[NSString stringWithUTF8String:sqlite3_column_name(statement.statement, columnIdx)]]; + [object setValue:s forKey:[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]]; } } } - (NSDictionary *)resultDict { - int num_cols = sqlite3_data_count(statement.statement); + int num_cols = sqlite3_data_count([_statement statement]); if (num_cols > 0) { NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols]; - if (!columnNamesSetup) { + if (!_columnNamesSetup) { [self setupColumnNames]; } - NSEnumerator *columnNames = [columnNameToIndexMap keyEnumerator]; + NSEnumerator *columnNames = [_columnNameToIndexMap keyEnumerator]; NSString *columnName = nil; while ((columnName = [columnNames nextObject])) { id objectValue = [self objectForColumnName:columnName]; [dict setObject:objectValue forKey:columnName]; } - return [[dict copy] autorelease]; + return FMDBReturnAutoreleased([dict copy]); } else { NSLog(@"Warning: There seem to be no columns in this set."); @@ -128,23 +130,23 @@ - (BOOL)next { do { retry = NO; - rc = sqlite3_step(statement.statement); + rc = sqlite3_step([_statement statement]); if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { // this will happen if the db is locked, like if we are doing an update or insert. // in that case, retry the step... and maybe wait just 10 milliseconds. retry = YES; if (SQLITE_LOCKED == rc) { - rc = sqlite3_reset(statement.statement); + rc = sqlite3_reset([_statement statement]); if (rc != SQLITE_LOCKED) { NSLog(@"Unexpected result from sqlite3_reset (%d) rs", rc); } } usleep(20); - if ([parentDB busyRetryTimeout] && (numberOfRetries++ > [parentDB busyRetryTimeout])) { + if ([_parentDB busyRetryTimeout] && (numberOfRetries++ > [_parentDB busyRetryTimeout])) { - NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [parentDB databasePath]); + NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [_parentDB databasePath]); NSLog(@"Database busy"); break; } @@ -153,17 +155,17 @@ - (BOOL)next { // all is well, let's return. } else if (SQLITE_ERROR == rc) { - NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([parentDB sqliteHandle])); + NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle])); break; } else if (SQLITE_MISUSE == rc) { // uh oh. - NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([parentDB sqliteHandle])); + NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle])); break; } else { // wtf? - NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([parentDB sqliteHandle])); + NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle])); break; } @@ -178,18 +180,18 @@ - (BOOL)next { } - (BOOL)hasAnotherRow { - return sqlite3_errcode([parentDB sqliteHandle]) == SQLITE_ROW; + return sqlite3_errcode([_parentDB sqliteHandle]) == SQLITE_ROW; } - (int)columnIndexForName:(NSString*)columnName { - if (!columnNamesSetup) { + if (!_columnNamesSetup) { [self setupColumnNames]; } columnName = [columnName lowercaseString]; - NSNumber *n = [columnNameToIndexMap objectForKey:columnName]; + NSNumber *n = [_columnNameToIndexMap objectForKey:columnName]; if (n) { return [n intValue]; @@ -207,7 +209,7 @@ - (int)intForColumn:(NSString*)columnName { } - (int)intForColumnIndex:(int)columnIdx { - return sqlite3_column_int(statement.statement, columnIdx); + return sqlite3_column_int([_statement statement], columnIdx); } - (long)longForColumn:(NSString*)columnName { @@ -215,7 +217,7 @@ - (long)longForColumn:(NSString*)columnName { } - (long)longForColumnIndex:(int)columnIdx { - return (long)sqlite3_column_int64(statement.statement, columnIdx); + return (long)sqlite3_column_int64([_statement statement], columnIdx); } - (long long int)longLongIntForColumn:(NSString*)columnName { @@ -223,7 +225,7 @@ - (long long int)longLongIntForColumn:(NSString*)columnName { } - (long long int)longLongIntForColumnIndex:(int)columnIdx { - return sqlite3_column_int64(statement.statement, columnIdx); + return sqlite3_column_int64([_statement statement], columnIdx); } - (BOOL)boolForColumn:(NSString*)columnName { @@ -239,16 +241,16 @@ - (double)doubleForColumn:(NSString*)columnName { } - (double)doubleForColumnIndex:(int)columnIdx { - return sqlite3_column_double(statement.statement, columnIdx); + return sqlite3_column_double([_statement statement], columnIdx); } - (NSString*)stringForColumnIndex:(int)columnIdx { - if (sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { return nil; } - const char *c = (const char *)sqlite3_column_text(statement.statement, columnIdx); + const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx); if (!c) { // null row. @@ -268,7 +270,7 @@ - (NSDate*)dateForColumn:(NSString*)columnName { - (NSDate*)dateForColumnIndex:(int)columnIdx { - if (sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { return nil; } @@ -282,15 +284,15 @@ - (NSData*)dataForColumn:(NSString*)columnName { - (NSData*)dataForColumnIndex:(int)columnIdx { - if (sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { return nil; } - int dataSize = sqlite3_column_bytes(statement.statement, columnIdx); + int dataSize = sqlite3_column_bytes([_statement statement], columnIdx); NSMutableData *data = [NSMutableData dataWithLength:dataSize]; - memcpy([data mutableBytes], sqlite3_column_blob(statement.statement, columnIdx), dataSize); + memcpy([data mutableBytes], sqlite3_column_blob([_statement statement], columnIdx), dataSize); return data; } @@ -302,20 +304,20 @@ - (NSData*)dataNoCopyForColumn:(NSString*)columnName { - (NSData*)dataNoCopyForColumnIndex:(int)columnIdx { - if (sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { return nil; } - int dataSize = sqlite3_column_bytes(statement.statement, columnIdx); + int dataSize = sqlite3_column_bytes([_statement statement], columnIdx); - NSData *data = [NSData dataWithBytesNoCopy:(void *)sqlite3_column_blob(statement.statement, columnIdx) length:dataSize freeWhenDone:NO]; + NSData *data = [NSData dataWithBytesNoCopy:(void *)sqlite3_column_blob([_statement statement], columnIdx) length:dataSize freeWhenDone:NO]; return data; } - (BOOL)columnIndexIsNull:(int)columnIdx { - return sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL; + return sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL; } - (BOOL)columnIsNull:(NSString*)columnName { @@ -324,11 +326,11 @@ - (BOOL)columnIsNull:(NSString*)columnName { - (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx { - if (sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { return nil; } - return sqlite3_column_text(statement.statement, columnIdx); + return sqlite3_column_text([_statement statement], columnIdx); } - (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName { @@ -336,7 +338,7 @@ - (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName { } - (id)objectForColumnIndex:(int)columnIdx { - int columnType = sqlite3_column_type(statement.statement, columnIdx); + int columnType = sqlite3_column_type([_statement statement], columnIdx); id returnValue = nil; @@ -367,11 +369,11 @@ - (id)objectForColumnName:(NSString*)columnName { // returns autoreleased NSString containing the name of the column in the result set - (NSString*)columnNameForIndex:(int)columnIdx { - return [NSString stringWithUTF8String: sqlite3_column_name(statement.statement, columnIdx)]; + return [NSString stringWithUTF8String: sqlite3_column_name([_statement statement], columnIdx)]; } - (void)setParentDB:(FMDatabase *)newDb { - parentDB = newDb; + _parentDB = newDb; } diff --git a/src/fmdb.m b/src/fmdb.m index c54838ef..3d5f67a8 100644 --- a/src/fmdb.m +++ b/src/fmdb.m @@ -1,31 +1,39 @@ #import #import "FMDatabase.h" #import "FMDatabaseAdditions.h" +#import "FMDatabasePool.h" +#import "FMDatabaseQueue.h" -#define FMDBQuickCheck(SomeBool) { if (!(SomeBool)) { NSLog(@"Failure on line %d", __LINE__); return 123; } } +#define FMDBQuickCheck(SomeBool) { if (!(SomeBool)) { NSLog(@"Failure on line %d", __LINE__); abort(); } } + +void testPool(NSString *dbPath); int main (int argc, const char * argv[]) { - NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; + +@autoreleasepool { + + + NSString *dbPath = @"/tmp/tmp.db"; // delete the old db. NSFileManager *fileManager = [NSFileManager defaultManager]; - [fileManager removeItemAtPath:@"/tmp/tmp.db" error:nil]; + [fileManager removeItemAtPath:dbPath error:nil]; - FMDatabase *db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"]; + FMDatabase *db = [FMDatabase databaseWithPath:dbPath]; - NSLog(@"Is SQLite compiled with it's thread safe options turned on? %@!", [FMDatabase isThreadSafe] ? @"Yes" : @"No"); + NSLog(@"Is SQLite compiled with it's thread safe options turned on? %@!", [FMDatabase isSQLiteThreadSafe] ? @"Yes" : @"No"); { - // ------------------------------------------------------------------------------- - // Un-opened database check. - FMDBQuickCheck([db executeQuery:@"select * from table"] == nil); - NSLog(@"%d: %@", [db lastErrorCode], [db lastErrorMessage]); - } + // ------------------------------------------------------------------------------- + // Un-opened database check. + FMDBQuickCheck([db executeQuery:@"select * from table"] == nil); + NSLog(@"%d: %@", [db lastErrorCode], [db lastErrorMessage]); + } if (![db open]) { NSLog(@"Could not open db."); - [pool release]; + return 0; } @@ -42,7 +50,7 @@ int main (int argc, const char * argv[]) { } NSError *err = 0x00; - FMDBQuickCheck(![db update:@"blah blah blah" error:&err bind:nil]); + FMDBQuickCheck(![db update:@"blah blah blah" withErrorAndBindings:&err]); FMDBQuickCheck(err != nil); FMDBQuickCheck([err code] == SQLITE_ERROR); NSLog(@"err: '%@'", err); @@ -157,9 +165,9 @@ int main (int argc, const char * argv[]) { // test the busy rety timeout schtuff. - [db setBusyRetryTimeout:50000]; + [db setBusyRetryTimeout:500]; - FMDatabase *newDb = [FMDatabase databaseWithPath:@"/tmp/tmp.db"]; + FMDatabase *newDb = [FMDatabase databaseWithPath:dbPath]; [newDb open]; rs = [newDb executeQuery:@"select rowid,* from test where a = ?", @"hi'"]; @@ -492,10 +500,95 @@ int main (int argc, const char * argv[]) { FMDBQuickCheck([[rs stringForColumn:@"c"] isEqualToString:@"BLOB"]); FMDBQuickCheck([[rs stringForColumn:@"d"] isEqualToString:@"d"]); FMDBQuickCheck(([rs longLongIntForColumn:@"e"] == 12345678901234)); + + [rs close]; + } + + + + { + FMDBQuickCheck([db executeUpdate:@"create table t55 (a text, b int, c float)"]); + short testShort = -4; + float testFloat = 5.5; + FMDBQuickCheck(([db executeUpdateWithFormat:@"insert into t55 values (%c, %hi, %g)", 'a', testShort, testFloat])); + + unsigned short testUShort = 6; + FMDBQuickCheck(([db executeUpdateWithFormat:@"insert into t55 values (%c, %hu, %g)", 'a', testUShort, testFloat])); + + + rs = [db executeQueryWithFormat:@"select * from t55 where a = %s order by 2", "a"]; + FMDBQuickCheck((rs != nil)); + + [rs next]; + + FMDBQuickCheck([[rs stringForColumn:@"a"] isEqualToString:@"a"]); + FMDBQuickCheck(([rs intForColumn:@"b"] == -4)); + FMDBQuickCheck([[rs stringForColumn:@"c"] isEqualToString:@"5.5"]); + + + [rs next]; + + FMDBQuickCheck([[rs stringForColumn:@"a"] isEqualToString:@"a"]); + FMDBQuickCheck(([rs intForColumn:@"b"] == 6)); + FMDBQuickCheck([[rs stringForColumn:@"c"] isEqualToString:@"5.5"]); + + [rs close]; + + } + + + + + { + NSError *err; + FMDBQuickCheck(([db update:@"insert into t5 values (?, ?, ?, ?, ?)" withErrorAndBindings:&err, @"text", [NSNumber numberWithInt:42], @"BLOB", @"d", [NSNumber numberWithInt:0]])); + } + { + // ------------------------------------------------------------------------------- + // Named parameters. + FMDBQuickCheck([db executeUpdate:@"create table namedparamtest (a text, b text, c integer, d double)"]); + NSMutableDictionary *dictionaryArgs = [NSMutableDictionary dictionary]; + [dictionaryArgs setObject:@"Text1" forKey:@"a"]; + [dictionaryArgs setObject:@"Text2" forKey:@"b"]; + [dictionaryArgs setObject:[NSNumber numberWithInt:1] forKey:@"c"]; + [dictionaryArgs setObject:[NSNumber numberWithDouble:2.0] forKey:@"d"]; + FMDBQuickCheck([db executeUpdate:@"insert into namedparamtest values (:a, :b, :c, :d)" withParameterDictionary:dictionaryArgs]); + + rs = [db executeQuery:@"select * from namedparamtest"]; + + FMDBQuickCheck((rs != nil)); + + [rs next]; + + FMDBQuickCheck([[rs stringForColumn:@"a"] isEqualToString:@"Text1"]); + FMDBQuickCheck([[rs stringForColumn:@"b"] isEqualToString:@"Text2"]); + FMDBQuickCheck([rs intForColumn:@"c"] == 1); + FMDBQuickCheck([rs doubleForColumn:@"d"] == 2.0); + + [rs close]; + + + dictionaryArgs = [NSMutableDictionary dictionary]; + + [dictionaryArgs setObject:@"Text2" forKey:@"blah"]; + + rs = [db executeQuery:@"select * from namedparamtest where b = :blah" withParameterDictionary:dictionaryArgs]; + + FMDBQuickCheck((rs != nil)); + FMDBQuickCheck([rs next]); + FMDBQuickCheck([[rs stringForColumn:@"b"] isEqualToString:@"Text2"]); + + [rs close]; + + + + + } + // just for fun. rs = [db executeQuery:@"PRAGMA database_list"]; while ([rs next]) { @@ -511,14 +604,496 @@ int main (int argc, const char * argv[]) { FMStatement *statement; while ((statement = [e nextObject])) { - NSLog(@"%@", statement); + NSLog(@"%@", statement); } } - NSLog(@"That was version %@ of sqlite", [FMDatabase sqliteLibVersion]); [db close]; - [pool release]; + + testPool(dbPath); + + + + FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:dbPath]; + + FMDBQuickCheck(queue); + + { + [queue inDatabase:^(FMDatabase *db) { + + + + [db executeUpdate:@"create table qfoo (foo text)"]; + [db executeUpdate:@"insert into qfoo values ('hi')"]; + [db executeUpdate:@"insert into qfoo values ('hello')"]; + [db executeUpdate:@"insert into qfoo values ('not')"]; + + + + int count = 0; + FMResultSet *rsl = [db executeQuery:@"select * from qfoo where foo like 'h%'"]; + while ([rsl next]) { + count++; + } + + FMDBQuickCheck(count == 2); + + count = 0; + rsl = [db executeQuery:@"select * from qfoo where foo like ?", @"h%"]; + while ([rsl next]) { + count++; + } + + FMDBQuickCheck(count == 2); + }]; + + } + + + { + // You should see pairs of numbers show up in stdout for this stuff: + int ops = 16; + + dispatch_queue_t dqueue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_HIGH); + + dispatch_apply(ops, dqueue, ^(size_t nby) { + + // just mix things up a bit for demonstration purposes. + if (nby % 2 == 1) { + [NSThread sleepForTimeInterval:.1]; + + [queue inTransaction:^(FMDatabase *db, BOOL *rollback) { + NSLog(@"Starting query %ld", nby); + + FMResultSet *rsl = [db executeQuery:@"select * from qfoo where foo like 'h%'"]; + while ([rsl next]) { + ;// whatever. + } + + NSLog(@"Ending query %ld", nby); + }]; + + } + + if (nby % 3 == 1) { + [NSThread sleepForTimeInterval:.1]; + } + + [queue inTransaction:^(FMDatabase *db, BOOL *rollback) { + NSLog(@"Starting update %ld", nby); + [db executeUpdate:@"insert into qfoo values ('1')"]; + [db executeUpdate:@"insert into qfoo values ('2')"]; + [db executeUpdate:@"insert into qfoo values ('3')"]; + NSLog(@"Ending update %ld", nby); + }]; + }); + + [queue close]; + + [queue inDatabase:^(FMDatabase *db) { + FMDBQuickCheck([db executeUpdate:@"insert into qfoo values ('1')"]); + }]; + } + + + + { + [queue inDatabase:^(FMDatabase *db) { + [db executeUpdate:@"create table transtest (a integer)"]; + FMDBQuickCheck([db executeUpdate:@"insert into transtest values (1)"]); + FMDBQuickCheck([db executeUpdate:@"insert into transtest values (2)"]); + + int rowCount = 0; + FMResultSet *rs = [db executeQuery:@"select * from transtest"]; + while ([rs next]) { + rowCount++; + } + + FMDBQuickCheck(rowCount == 2); + }]; + + + + [queue inTransaction:^(FMDatabase *db, BOOL *rollback) { + FMDBQuickCheck([db executeUpdate:@"insert into transtest values (3)"]); + + if (YES) { + // uh oh!, something went wrong (not really, this is just a test + *rollback = YES; + return; + } + + FMDBQuickCheck([db executeUpdate:@"insert into transtest values (4)"]); + }]; + + [queue inDatabase:^(FMDatabase *db) { + + int rowCount = 0; + FMResultSet *rs = [db executeQuery:@"select * from transtest"]; + while ([rs next]) { + rowCount++; + } + + NSLog(@"after rollback, rowCount is %d (should be 2)", rowCount); + + FMDBQuickCheck(rowCount == 2); + }]; + } + + // hey, let's make a custom function! + + [queue inDatabase:^(FMDatabase *db) { + + [db executeUpdate:@"create table ftest (foo text)"]; + [db executeUpdate:@"insert into ftest values ('hello')"]; + [db executeUpdate:@"insert into ftest values ('hi')"]; + [db executeUpdate:@"insert into ftest values ('not h!')"]; + [db executeUpdate:@"insert into ftest values ('definitely not h!')"]; + + [db makeFunctionNamed:@"StringStartsWithH" maximumArguments:1 withBlock:^(sqlite3_context *context, int argc, sqlite3_value **argv) { + if (sqlite3_value_type(argv[0]) == SQLITE_TEXT) { + + @autoreleasepool { + + const char *c = (const char *)sqlite3_value_text(argv[0]); + + NSString *s = [NSString stringWithUTF8String:c]; + + sqlite3_result_int(context, [s hasPrefix:@"h"]); + } + } + else { + NSLog(@"Unknown formart for StringStartsWithH (%d) %s:%d", sqlite3_value_type(argv[0]), __FUNCTION__, __LINE__); + sqlite3_result_null(context); + } + }]; + + int rowCount = 0; + FMResultSet *rs = [db executeQuery:@"select * from ftest where StringStartsWithH(foo)"]; + while ([rs next]) { + rowCount++; + + NSLog(@"Does %@ start with 'h'?", [rs stringForColumnIndex:0]); + + } + FMDBQuickCheck(rowCount == 2); + + + + + + + }]; + + + NSLog(@"That was version %@ of sqlite", [FMDatabase sqliteLibVersion]); + + +}// this is the end of our @autorelease pool. + + return 0; } + +/* + Test the various FMDatabasePool things. +*/ + +void testPool(NSString *dbPath) { + + FMDatabasePool *dbPool = [FMDatabasePool databasePoolWithPath:dbPath]; + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 0); + + __block FMDatabase *db1; + + [dbPool inDatabase:^(FMDatabase *db) { + + + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 1); + + FMDBQuickCheck([db tableExists:@"t4"]); + + db1 = db; + + }]; + + [dbPool inDatabase:^(FMDatabase *db) { + FMDBQuickCheck(db1 == db); + + [dbPool inDatabase:^(FMDatabase *db2) { + FMDBQuickCheck(db2 != db); + }]; + + }]; + + + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 2); + + + [dbPool inDatabase:^(FMDatabase *db) { + [db executeUpdate:@"create table easy (a text)"]; + [db executeUpdate:@"create table easy2 (a text)"]; + + }]; + + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 2); + + [dbPool releaseAllDatabases]; + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 0); + + [dbPool inDatabase:^(FMDatabase *aDb) { + + FMDBQuickCheck([dbPool countOfCheckedInDatabases] == 0); + FMDBQuickCheck([dbPool countOfCheckedOutDatabases] == 1); + + FMDBQuickCheck([aDb tableExists:@"t4"]); + + FMDBQuickCheck([dbPool countOfCheckedInDatabases] == 0); + FMDBQuickCheck([dbPool countOfCheckedOutDatabases] == 1); + + FMDBQuickCheck(([aDb executeUpdate:@"insert into easy (a) values (?)", @"hi"])); + + // just for fun. + FMResultSet *rs2 = [aDb executeQuery:@"select * from easy"]; + FMDBQuickCheck([rs2 next]); + while ([rs2 next]) { ; } // whatevers. + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 1); + FMDBQuickCheck([dbPool countOfCheckedInDatabases] == 0); + FMDBQuickCheck([dbPool countOfCheckedOutDatabases] == 1); + }]; + + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 1); + + + { + + [dbPool inDatabase:^(FMDatabase *db) { + + [db executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1]]; + [db executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:2]]; + [db executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:3]]; + + FMDBQuickCheck([dbPool countOfCheckedInDatabases] == 0); + FMDBQuickCheck([dbPool countOfCheckedOutDatabases] == 1); + }]; + } + + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 1); + + [dbPool setMaximumNumberOfDatabasesToCreate:2]; + + + [dbPool inDatabase:^(FMDatabase *db) { + [dbPool inDatabase:^(FMDatabase *db2) { + [dbPool inDatabase:^(FMDatabase *db3) { + FMDBQuickCheck([dbPool countOfOpenDatabases] == 2); + FMDBQuickCheck(!db3); + }]; + + }]; + }]; + + [dbPool setMaximumNumberOfDatabasesToCreate:0]; + + [dbPool releaseAllDatabases]; + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 0); + + [dbPool inDatabase:^(FMDatabase *db) { + [db executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:3]]; + }]; + + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 1); + + + [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]]; + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 1); + FMDBQuickCheck([dbPool countOfCheckedInDatabases] == 0); + FMDBQuickCheck([dbPool countOfCheckedOutDatabases] == 1); + }]; + + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 1); + FMDBQuickCheck([dbPool countOfCheckedInDatabases] == 1); + FMDBQuickCheck([dbPool countOfCheckedOutDatabases] == 0); + + + [dbPool inDatabase:^(FMDatabase *db) { + FMResultSet *rs2 = [db executeQuery:@"select * from easy where a = ?", [NSNumber numberWithInt:1001]]; + FMDBQuickCheck([rs2 next]); + FMDBQuickCheck(![rs2 next]); + }]; + + + + [dbPool inDeferredTransaction:^(FMDatabase *adb, BOOL *rollback) { + [adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1004]]; + [adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1005]]; + + *rollback = YES; + }]; + + FMDBQuickCheck([dbPool countOfOpenDatabases] == 1); + FMDBQuickCheck([dbPool countOfCheckedInDatabases] == 1); + FMDBQuickCheck([dbPool countOfCheckedOutDatabases] == 0); + + NSError *err = [dbPool inSavePoint:^(FMDatabase *db, BOOL *rollback) { + [db executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1006]]; + }]; + + FMDBQuickCheck(!err); + + { + + 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; + }]; + }]; + + + FMDBQuickCheck(!err); + + [dbPool inDatabase:^(FMDatabase *db) { + + + FMResultSet *rs2 = [db executeQuery:@"select * from easy where a = ?", [NSNumber numberWithInt:1009]]; + FMDBQuickCheck([rs2 next]); + FMDBQuickCheck(![rs2 next]); // close it out. + + rs2 = [db executeQuery:@"select * from easy where a = ?", [NSNumber numberWithInt:1010]]; + FMDBQuickCheck(![rs2 next]); + }]; + + + } + + { + + [dbPool inDatabase:^(FMDatabase *db) { + [db executeUpdate:@"create table likefoo (foo text)"]; + [db executeUpdate:@"insert into likefoo values ('hi')"]; + [db executeUpdate:@"insert into likefoo values ('hello')"]; + [db executeUpdate:@"insert into likefoo values ('not')"]; + + int count = 0; + FMResultSet *rsl = [db executeQuery:@"select * from likefoo where foo like 'h%'"]; + while ([rsl next]) { + count++; + } + + FMDBQuickCheck(count == 2); + + count = 0; + rsl = [db executeQuery:@"select * from likefoo where foo like ?", @"h%"]; + while ([rsl next]) { + count++; + } + + FMDBQuickCheck(count == 2); + + }]; + } + + + { + + int ops = 128; + + dispatch_queue_t dqueue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_HIGH); + + dispatch_apply(ops, dqueue, ^(size_t nby) { + + // just mix things up a bit for demonstration purposes. + if (nby % 2 == 1) { + + [NSThread sleepForTimeInterval:.1]; + } + + [dbPool inDatabase:^(FMDatabase *db) { + NSLog(@"Starting query %ld", nby); + + FMResultSet *rsl = [db executeQuery:@"select * from likefoo where foo like 'h%'"]; + while ([rsl next]) { + if (nby % 3 == 1) { + [NSThread sleepForTimeInterval:.05]; + } + } + + NSLog(@"Ending query %ld", nby); + }]; + }); + + NSLog(@"Number of open databases after crazy gcd stuff: %ld", [dbPool countOfOpenDatabases]); + } + + + // if you want to see a deadlock, just uncomment this line and run: + //#define ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD 1 +#ifdef ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD + { + + int ops = 16; + + dispatch_queue_t dqueue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_HIGH); + + dispatch_apply(ops, dqueue, ^(size_t nby) { + + // just mix things up a bit for demonstration purposes. + if (nby % 2 == 1) { + [NSThread sleepForTimeInterval:.1]; + + [dbPool inTransaction:^(FMDatabase *db, BOOL *rollback) { + NSLog(@"Starting query %ld", nby); + + FMResultSet *rsl = [db executeQuery:@"select * from likefoo where foo like 'h%'"]; + while ([rsl next]) { + ;// whatever. + } + + NSLog(@"Ending query %ld", nby); + }]; + + } + + if (nby % 3 == 1) { + [NSThread sleepForTimeInterval:.1]; + } + + [dbPool inTransaction:^(FMDatabase *db, BOOL *rollback) { + NSLog(@"Starting update %ld", nby); + [db executeUpdate:@"insert into likefoo values ('1')"]; + [db executeUpdate:@"insert into likefoo values ('2')"]; + [db executeUpdate:@"insert into likefoo values ('3')"]; + NSLog(@"Ending update %ld", nby); + }]; + }); + + [dbPool releaseAllDatabases]; + + [dbPool inDatabase:^(FMDatabase *db) { + FMDBQuickCheck([db executeUpdate:@"insert into likefoo values ('1')"]); + }]; + } +#endif + +}