From c16b6dec2a551209692a3144320d8a010a96eeab Mon Sep 17 00:00:00 2001 From: Samuel Clay Date: Fri, 31 May 2013 15:31:40 -0700 Subject: [PATCH] Adding fmdb. --- media/ios/NewsBlur.xcodeproj/project.pbxproj | 43 + media/ios/Other Sources/fmdb/FMDatabase.h | 218 +++ media/ios/Other Sources/fmdb/FMDatabase.m | 1227 ++++++++++++++ .../Other Sources/fmdb/FMDatabaseAdditions.h | 47 + .../Other Sources/fmdb/FMDatabaseAdditions.m | 209 +++ media/ios/Other Sources/fmdb/FMDatabasePool.h | 75 + media/ios/Other Sources/fmdb/FMDatabasePool.m | 244 +++ .../ios/Other Sources/fmdb/FMDatabaseQueue.h | 38 + .../ios/Other Sources/fmdb/FMDatabaseQueue.m | 176 ++ media/ios/Other Sources/fmdb/FMResultSet.h | 104 ++ media/ios/Other Sources/fmdb/FMResultSet.m | 413 +++++ media/ios/Other Sources/fmdb/fmdb.m | 1455 +++++++++++++++++ 12 files changed, 4249 insertions(+) create mode 100755 media/ios/Other Sources/fmdb/FMDatabase.h create mode 100755 media/ios/Other Sources/fmdb/FMDatabase.m create mode 100755 media/ios/Other Sources/fmdb/FMDatabaseAdditions.h create mode 100755 media/ios/Other Sources/fmdb/FMDatabaseAdditions.m create mode 100755 media/ios/Other Sources/fmdb/FMDatabasePool.h create mode 100755 media/ios/Other Sources/fmdb/FMDatabasePool.m create mode 100755 media/ios/Other Sources/fmdb/FMDatabaseQueue.h create mode 100755 media/ios/Other Sources/fmdb/FMDatabaseQueue.m create mode 100755 media/ios/Other Sources/fmdb/FMResultSet.h create mode 100755 media/ios/Other Sources/fmdb/FMResultSet.m create mode 100755 media/ios/Other Sources/fmdb/fmdb.m diff --git a/media/ios/NewsBlur.xcodeproj/project.pbxproj b/media/ios/NewsBlur.xcodeproj/project.pbxproj index e2d97a65e..f16cc473f 100755 --- a/media/ios/NewsBlur.xcodeproj/project.pbxproj +++ b/media/ios/NewsBlur.xcodeproj/project.pbxproj @@ -270,6 +270,12 @@ FF688E5316E6B8D0003B7B42 /* traverse_background@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FF688E5116E6B8D0003B7B42 /* traverse_background@2x.png */; }; FF6A233216448E0700E15989 /* StoryPageControl.m in Sources */ = {isa = PBXBuildFile; fileRef = FF6A233116448E0700E15989 /* StoryPageControl.m */; }; FF6A23391644957800E15989 /* StoryPageControl.xib in Resources */ = {isa = PBXBuildFile; fileRef = FF6A23361644903900E15989 /* StoryPageControl.xib */; }; + FF753CCE175858FC00344EC9 /* FMDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = FF753CC4175858FC00344EC9 /* FMDatabase.m */; }; + FF753CCF175858FC00344EC9 /* FMDatabaseAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = FF753CC6175858FC00344EC9 /* FMDatabaseAdditions.m */; }; + FF753CD0175858FC00344EC9 /* FMDatabasePool.m in Sources */ = {isa = PBXBuildFile; fileRef = FF753CC8175858FC00344EC9 /* FMDatabasePool.m */; }; + FF753CD1175858FC00344EC9 /* FMDatabaseQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = FF753CCA175858FC00344EC9 /* FMDatabaseQueue.m */; }; + FF753CD2175858FC00344EC9 /* fmdb.m in Sources */ = {isa = PBXBuildFile; fileRef = FF753CCB175858FC00344EC9 /* fmdb.m */; }; + FF753CD3175858FC00344EC9 /* FMResultSet.m in Sources */ = {isa = PBXBuildFile; fileRef = FF753CCD175858FC00344EC9 /* FMResultSet.m */; }; FF793E1B13F1A9F700F282D2 /* ASIDataCompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = FF793E1813F1A9F700F282D2 /* ASIDataCompressor.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; FF793E1C13F1A9F700F282D2 /* ASIDataDecompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = FF793E1A13F1A9F700F282D2 /* ASIDataDecompressor.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; FF8364BB1755759A008F5C58 /* traverse_text.png in Resources */ = {isa = PBXBuildFile; fileRef = FF8364B91755759A008F5C58 /* traverse_text.png */; }; @@ -800,6 +806,17 @@ FF6A233016448E0700E15989 /* StoryPageControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StoryPageControl.h; sourceTree = ""; }; FF6A233116448E0700E15989 /* StoryPageControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StoryPageControl.m; sourceTree = ""; }; FF6A23361644903900E15989 /* StoryPageControl.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = StoryPageControl.xib; path = ../Classes/StoryPageControl.xib; sourceTree = ""; }; + FF753CC3175858FC00344EC9 /* FMDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabase.h; sourceTree = ""; }; + FF753CC4175858FC00344EC9 /* FMDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabase.m; sourceTree = ""; }; + FF753CC5175858FC00344EC9 /* FMDatabaseAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabaseAdditions.h; sourceTree = ""; }; + FF753CC6175858FC00344EC9 /* FMDatabaseAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabaseAdditions.m; sourceTree = ""; }; + FF753CC7175858FC00344EC9 /* FMDatabasePool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabasePool.h; sourceTree = ""; }; + FF753CC8175858FC00344EC9 /* FMDatabasePool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabasePool.m; sourceTree = ""; }; + FF753CC9175858FC00344EC9 /* FMDatabaseQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabaseQueue.h; sourceTree = ""; }; + FF753CCA175858FC00344EC9 /* FMDatabaseQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabaseQueue.m; sourceTree = ""; }; + FF753CCB175858FC00344EC9 /* fmdb.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = fmdb.m; sourceTree = ""; }; + FF753CCC175858FC00344EC9 /* FMResultSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMResultSet.h; sourceTree = ""; }; + FF753CCD175858FC00344EC9 /* FMResultSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMResultSet.m; sourceTree = ""; }; FF793E1713F1A9F700F282D2 /* ASIDataCompressor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIDataCompressor.h; sourceTree = ""; }; FF793E1813F1A9F700F282D2 /* ASIDataCompressor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIDataCompressor.m; sourceTree = ""; }; FF793E1913F1A9F700F282D2 /* ASIDataDecompressor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIDataDecompressor.h; sourceTree = ""; }; @@ -1137,6 +1154,7 @@ 29B97315FDCFA39411CA2CEA /* Other Sources */ = { isa = PBXGroup; children = ( + FF753CC2175858FC00344EC9 /* fmdb */, FFDC9F9616E8064C000D8E0C /* ShareThis */, FF22FE5316E53ADC0046165A /* Underscore */, 43A4BABE15C8663600F3B8D4 /* Popover */, @@ -1779,6 +1797,25 @@ path = Reachability; sourceTree = ""; }; + FF753CC2175858FC00344EC9 /* fmdb */ = { + isa = PBXGroup; + children = ( + FF753CC3175858FC00344EC9 /* FMDatabase.h */, + FF753CC4175858FC00344EC9 /* FMDatabase.m */, + FF753CC5175858FC00344EC9 /* FMDatabaseAdditions.h */, + FF753CC6175858FC00344EC9 /* FMDatabaseAdditions.m */, + FF753CC7175858FC00344EC9 /* FMDatabasePool.h */, + FF753CC8175858FC00344EC9 /* FMDatabasePool.m */, + FF753CC9175858FC00344EC9 /* FMDatabaseQueue.h */, + FF753CCA175858FC00344EC9 /* FMDatabaseQueue.m */, + FF753CCB175858FC00344EC9 /* fmdb.m */, + FF753CCC175858FC00344EC9 /* FMResultSet.h */, + FF753CCD175858FC00344EC9 /* FMResultSet.m */, + ); + name = fmdb; + path = "Other Sources/fmdb"; + sourceTree = ""; + }; FF9B565E16E810900054BF68 /* Assets */ = { isa = PBXGroup; children = ( @@ -2500,6 +2537,12 @@ FFDCA0BC16E80877000D8E0C /* NSData+Base64.m in Sources */, FFECD016172B0D4200D45A62 /* SafariService.m in Sources */, FFECD01D172B10BD00D45A62 /* SafariActivityItem.m in Sources */, + FF753CCE175858FC00344EC9 /* FMDatabase.m in Sources */, + FF753CCF175858FC00344EC9 /* FMDatabaseAdditions.m in Sources */, + FF753CD0175858FC00344EC9 /* FMDatabasePool.m in Sources */, + FF753CD1175858FC00344EC9 /* FMDatabaseQueue.m in Sources */, + FF753CD2175858FC00344EC9 /* fmdb.m in Sources */, + FF753CD3175858FC00344EC9 /* FMResultSet.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/media/ios/Other Sources/fmdb/FMDatabase.h b/media/ios/Other Sources/fmdb/FMDatabase.h new file mode 100755 index 000000000..15d636257 --- /dev/null +++ b/media/ios/Other Sources/fmdb/FMDatabase.h @@ -0,0 +1,218 @@ +#import +#import "sqlite3.h" +#import "FMResultSet.h" +#import "FMDatabasePool.h" + + +#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]); + + #define FMDBDispatchQueueRelease(__v) (dispatch_release(__v)); +#else + // -fobjc-arc + #define FMDBAutorelease(__v) + #define FMDBReturnAutoreleased(__v) (__v) + + #define FMDBRetain(__v) + #define FMDBReturnRetained(__v) (__v) + + #define FMDBRelease(__v) + + #if TARGET_OS_IPHONE + // Compiling for iOS + #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 + // iOS 6.0 or later + #define FMDBDispatchQueueRelease(__v) + #else + // iOS 5.X or earlier + #define FMDBDispatchQueueRelease(__v) (dispatch_release(__v)); + #endif + #else + // Compiling for Mac OS X + #if MAC_OS_X_VERSION_MIN_REQUIRED >= 1080 + // Mac OS X 10.8 or later + #define FMDBDispatchQueueRelease(__v) + #else + // Mac OS X 10.7 or earlier + #define FMDBDispatchQueueRelease(__v) (dispatch_release(__v)); + #endif + #endif +#endif + + +@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; + + NSDateFormatter *_dateFormat; +} + + +@property (atomic, assign) BOOL traceExecution; +@property (atomic, assign) BOOL checkedOut; +@property (atomic, assign) int busyRetryTimeout; +@property (atomic, assign) BOOL crashOnErrors; +@property (atomic, assign) BOOL logsErrors; +@property (atomic, retain) NSMutableDictionary *cachedStatements; + ++ (id)databaseWithPath:(NSString*)inPath; +- (id)initWithPath:(NSString*)inPath; + +- (BOOL)open; +#if SQLITE_VERSION_NUMBER >= 3005000 +- (BOOL)openWithFlags:(int)flags; +#endif +- (BOOL)close; +- (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; +- (BOOL)setKeyWithData:(NSData *)keyData; +- (BOOL)rekeyWithData:(NSData *)keyData; + +- (NSString *)databasePath; + +- (NSString*)lastErrorMessage; + +- (int)lastErrorCode; +- (BOOL)hadError; +- (NSError*)lastError; + +- (sqlite_int64)lastInsertRowId; + +- (sqlite3*)sqliteHandle; + +- (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 withParameterDictionary:(NSDictionary *)arguments; + +- (FMResultSet *)executeQuery:(NSString*)sql, ...; +- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...; +- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments; +- (FMResultSet *)executeQuery:(NSString *)sql withParameterDictionary:(NSDictionary *)arguments; + +- (BOOL)rollback; +- (BOOL)commit; +- (BOOL)beginTransaction; +- (BOOL)beginDeferredTransaction; +- (BOOL)inTransaction; +- (BOOL)shouldCacheStatements; +- (void)setShouldCacheStatements:(BOOL)value; + +#if SQLITE_VERSION_NUMBER >= 3007000 +- (BOOL)startSavePointWithName:(NSString*)name error:(NSError**)outErr; +- (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError**)outErr; +- (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError**)outErr; +- (NSError*)inSavePoint:(void (^)(BOOL *rollback))block; +#endif + ++ (BOOL)isSQLiteThreadSafe; ++ (NSString*)sqliteLibVersion; + +- (int)changes; + +- (void)makeFunctionNamed:(NSString*)name maximumArguments:(int)count withBlock:(void (^)(sqlite3_context *context, int argc, sqlite3_value **argv))block; + + + +/** Generate an NSDateFormat that won't be broken by timezone or locale changes. + + Use this method to generate values to set the dateFormat property. + + @param dateFormat A valid NSDateFormatter format string. + + Example: + + myDB.dateFormat = [FMDatabase storeableDateFormat:@"yyyy-MM-dd HH:mm:ss"]; + + Note that NSDateFormatter is not thread-safe, so the formatter generated by this method should be assigned to only one FMDB instance and should not be used for other purposes. + + */ ++ (NSDateFormatter *)storeableDateFormat:(NSString *)format; + + +/** Test whether the database has a date formatter assigned. + + */ +- (BOOL)hasDateFormatter; + + +/** Set to a date formatter to use string dates with sqlite instead of the default UNIX timestamps. + + Set to nil to use UNIX timestamps. + + Defaults to nil. + + Should be set using a formatter generated using FMDatabase::storeableDateFormat. + + Note there is no direct getter for the NSDateFormatter, and you should not use the formatter you pass to FMDB for other purposes, as NSDateFormatter is not thread-safe. + + */ +- (void)setDateFormat:(NSDateFormatter *)format; + + +/** Convert the supplied NSString to NSDate, using the current database formatter. + + Returns nil if no formatter is set. + + */ +- (NSDate *)dateFromString:(NSString *)s; + +/** Convert the supplied NSDate to NSString, using the current database formatter. + + Returns nil if no formatter is set. + + */ +- (NSString *)stringFromDate:(NSDate *)date; + + + + + + + + + + +@end + +@interface FMStatement : NSObject { + sqlite3_stmt *_statement; + NSString *_query; + long _useCount; +} + +@property (atomic, assign) long useCount; +@property (atomic, retain) NSString *query; +@property (atomic, assign) sqlite3_stmt *statement; + +- (void)close; +- (void)reset; + +@end + diff --git a/media/ios/Other Sources/fmdb/FMDatabase.m b/media/ios/Other Sources/fmdb/FMDatabase.m new file mode 100755 index 000000000..aec6d5239 --- /dev/null +++ b/media/ios/Other Sources/fmdb/FMDatabase.m @@ -0,0 +1,1227 @@ +#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 cachedStatements=_cachedStatements; +@synthesize logsErrors=_logsErrors; +@synthesize crashOnErrors=_crashOnErrors; +@synthesize busyRetryTimeout=_busyRetryTimeout; +@synthesize checkedOut=_checkedOut; +@synthesize traceExecution=_traceExecution; + ++ (id)databaseWithPath:(NSString*)aPath { + 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() != 0; +} + +- (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; + } + + return self; +} + +- (void)finalize { + [self close]; + [super finalize]; +} + +- (void)dealloc { + [self close]; + FMDBRelease(_openResultSets); + FMDBRelease(_cachedStatements); + FMDBRelease(_dateFormat); + FMDBRelease(_databasePath); + FMDBRelease(_openFunctions); + +#if ! __has_feature(objc_arc) + [super dealloc]; +#endif +} + +- (NSString *)databasePath { + return _databasePath; +} + +- (sqlite3*)sqliteHandle { + return _db; +} + +- (const char*)sqlitePath { + + if (!_databasePath) { + return ":memory:"; + } + + if ([_databasePath length] == 0) { + return ""; // this creates a temporary database (it's an sqlite thing). + } + + return [_databasePath fileSystemRepresentation]; + +} + +- (BOOL)open { + if (_db) { + return YES; + } + + int err = sqlite3_open([self sqlitePath], &_db ); + if(err != SQLITE_OK) { + NSLog(@"error opening!: %d", err); + return NO; + } + + return YES; +} + +#if SQLITE_VERSION_NUMBER >= 3005000 +- (BOOL)openWithFlags:(int)flags { + int err = sqlite3_open_v2([self sqlitePath], &_db, flags, NULL /* Name of VFS module to use */); + if(err != SQLITE_OK) { + NSLog(@"error opening!: %d", err); + return NO; + } + return YES; +} +#endif + + +- (BOOL)close { + + [self clearCachedStatements]; + [self closeOpenResultSets]; + + if (!_db) { + return YES; + } + + int rc; + BOOL retry; + int numberOfRetries = 0; + BOOL triedFinalizingOpenStatements = NO; + + do { + retry = NO; + rc = sqlite3_close(_db); + + if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { + + retry = YES; + usleep(20); + + if (_busyRetryTimeout && (numberOfRetries++ > _busyRetryTimeout)) { + NSLog(@"%s:%d", __FUNCTION__, __LINE__); + NSLog(@"Database busy, unable to close"); + return NO; + } + + if (!triedFinalizingOpenStatements) { + triedFinalizingOpenStatements = YES; + sqlite3_stmt *pStmt; + while ((pStmt = sqlite3_next_stmt(_db, 0x00)) !=0) { + NSLog(@"Closing leaked statement"); + sqlite3_finalize(pStmt); + } + } + } + else if (SQLITE_OK != rc) { + NSLog(@"error closing!: %d", rc); + } + } + while (retry); + + _db = nil; + return YES; +} + +- (void)clearCachedStatements { + + for (FMStatement *cachedStmt in [_cachedStatements objectEnumerator]) { + [cachedStmt close]; + } + + [_cachedStatements removeAllObjects]; +} + +- (BOOL)hasOpenResultSets { + return [_openResultSets count] > 0; +} + +- (void)closeOpenResultSets { + + //Copy the set so we don't get mutation errors + NSSet *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]; +} + +- (FMStatement*)cachedStatementForQuery:(NSString*)query { + return [_cachedStatements objectForKey:query]; +} + +- (void)setCachedStatement:(FMStatement*)statement forQuery:(NSString*)query { + + query = [query copy]; // in case we got handed in a mutable string... + + [statement setQuery:query]; + + [_cachedStatements setObject:statement forKey:query]; + + FMDBRelease(query); +} + + +- (BOOL)rekey:(NSString*)key { + NSData *keyData = [NSData dataWithBytes:(void *)[key UTF8String] length:(NSUInteger)strlen([key UTF8String])]; + + return [self rekeyWithData:keyData]; +} + +- (BOOL)rekeyWithData:(NSData *)keyData { +#ifdef SQLITE_HAS_CODEC + if (!keyData) { + return NO; + } + + int rc = sqlite3_rekey(_db, [keyData bytes], (int)[keyData length]); + + if (rc != SQLITE_OK) { + NSLog(@"error on rekey: %d", rc); + NSLog(@"%@", [self lastErrorMessage]); + } + + return (rc == SQLITE_OK); +#else + return NO; +#endif +} + +- (BOOL)setKey:(NSString*)key { + NSData *keyData = [NSData dataWithBytes:[key UTF8String] length:(NSUInteger)strlen([key UTF8String])]; + + return [self setKeyWithData:keyData]; +} + +- (BOOL)setKeyWithData:(NSData *)keyData { +#ifdef SQLITE_HAS_CODEC + if (!keyData) { + return NO; + } + + int rc = sqlite3_key(_db, [keyData bytes], (int)[keyData length]); + + return (rc == SQLITE_OK); +#else + return NO; +#endif +} + ++ (NSDateFormatter *)storeableDateFormat:(NSString *)format { + + NSDateFormatter *result = FMDBReturnAutoreleased([[NSDateFormatter alloc] init]); + result.dateFormat = format; + result.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + result.locale = FMDBReturnAutoreleased([[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]); + return result; +} + + +- (BOOL)hasDateFormatter { + return _dateFormat != nil; +} + +- (void)setDateFormat:(NSDateFormatter *)format { + FMDBAutorelease(_dateFormat); + _dateFormat = FMDBReturnRetained(format); +} + +- (NSDate *)dateFromString:(NSString *)s { + return [_dateFormat dateFromString:s]; +} + +- (NSString *)stringFromDate:(NSDate *)date { + return [_dateFormat stringFromDate:date]; +} + + +- (BOOL)goodConnection { + + if (!_db) { + return NO; + } + + FMResultSet *rs = [self executeQuery:@"select name from sqlite_master where type='table'"]; + + if (rs) { + [rs close]; + return YES; + } + + return NO; +} + +- (void)warnInUse { + NSLog(@"The FMDatabase %@ is currently in use.", self); + +#ifndef NS_BLOCK_ASSERTIONS + if (_crashOnErrors) { + NSAssert1(false, @"The FMDatabase %@ is currently in use.", self); + abort(); + } +#endif +} + +- (BOOL)databaseExists { + + if (!_db) { + + NSLog(@"The FMDatabase %@ is not open.", self); + + #ifndef NS_BLOCK_ASSERTIONS + if (_crashOnErrors) { + NSAssert1(false, @"The FMDatabase %@ is not open.", self); + abort(); + } + #endif + + return NO; + } + + return YES; +} + +- (NSString*)lastErrorMessage { + return [NSString stringWithUTF8String:sqlite3_errmsg(_db)]; +} + +- (BOOL)hadError { + int lastErrCode = [self lastErrorCode]; + + return (lastErrCode > SQLITE_OK && lastErrCode < SQLITE_ROW); +} + +- (int)lastErrorCode { + return sqlite3_errcode(_db); +} + + +- (NSError*)errorWithMessage:(NSString*)message { + NSDictionary* errorMessage = [NSDictionary dictionaryWithObject:message forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:@"FMDatabase" code:sqlite3_errcode(_db) userInfo:errorMessage]; +} + +- (NSError*)lastError { + return [self errorWithMessage:[self lastErrorMessage]]; +} + +- (sqlite_int64)lastInsertRowId { + + if (_isExecutingStatement) { + [self warnInUse]; + return NO; + } + + _isExecutingStatement = YES; + + sqlite_int64 ret = sqlite3_last_insert_rowid(_db); + + _isExecutingStatement = NO; + + return ret; +} + +- (int)changes { + if (_isExecutingStatement) { + [self warnInUse]; + return 0; + } + + _isExecutingStatement = YES; + + int ret = sqlite3_changes(_db); + + _isExecutingStatement = NO; + + return ret; +} + +- (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt { + + if ((!obj) || ((NSNull *)obj == [NSNull null])) { + sqlite3_bind_null(pStmt, idx); + } + + // FIXME - someday check the return codes on these binds. + else if ([obj isKindOfClass:[NSData class]]) { + const void *bytes = [obj bytes]; + if (!bytes) { + // it's an empty NSData object, aka [NSData data]. + // Don't pass a NULL pointer, or sqlite will bind a SQL null instead of a blob. + bytes = ""; + } + sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_STATIC); + } + else if ([obj isKindOfClass:[NSDate class]]) { + if (self.hasDateFormatter) + sqlite3_bind_text(pStmt, idx, [[self stringFromDate:obj] UTF8String], -1, SQLITE_STATIC); + else + sqlite3_bind_double(pStmt, idx, [obj timeIntervalSince1970]); + } + else if ([obj isKindOfClass:[NSNumber class]]) { + + if (strcmp([obj objCType], @encode(BOOL)) == 0) { + sqlite3_bind_int(pStmt, idx, ([obj boolValue] ? 1 : 0)); + } + else if (strcmp([obj objCType], @encode(int)) == 0) { + sqlite3_bind_int64(pStmt, idx, [obj longValue]); + } + else if (strcmp([obj objCType], @encode(long)) == 0) { + sqlite3_bind_int64(pStmt, idx, [obj longValue]); + } + else if (strcmp([obj objCType], @encode(long long)) == 0) { + sqlite3_bind_int64(pStmt, idx, [obj longLongValue]); + } + else if (strcmp([obj objCType], @encode(unsigned long long)) == 0) { + sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongLongValue]); + } + else if (strcmp([obj objCType], @encode(float)) == 0) { + sqlite3_bind_double(pStmt, idx, [obj floatValue]); + } + else if (strcmp([obj objCType], @encode(double)) == 0) { + sqlite3_bind_double(pStmt, idx, [obj doubleValue]); + } + else { + sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC); + } + } + else { + sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC); + } +} + +- (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments { + + NSUInteger length = [sql length]; + unichar last = '\0'; + for (NSUInteger i = 0; i < length; ++i) { + id arg = nil; + unichar current = [sql characterAtIndex:i]; + unichar add = current; + if (last == '%') { + switch (current) { + 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; + case 'd': + case 'D': + case 'i': + arg = [NSNumber numberWithInt:va_arg(args, int)]; + break; + case 'u': + case 'U': + arg = [NSNumber numberWithUnsignedInt:va_arg(args, unsigned int)]; + break; + case 'h': + i++; + if (i < length && [sql characterAtIndex:i] == 'i') { + // 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:(short)(va_arg(args, int))]; + } + else if (i < length && [sql characterAtIndex:i] == 'u') { + // 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:(unsigned short)(va_arg(args, uint))]; + } + else { + i--; + } + break; + case 'q': + i++; + if (i < length && [sql characterAtIndex:i] == 'i') { + arg = [NSNumber numberWithLongLong:va_arg(args, long long)]; + } + else if (i < length && [sql characterAtIndex:i] == 'u') { + arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)]; + } + else { + i--; + } + break; + case 'f': + arg = [NSNumber numberWithDouble:va_arg(args, double)]; + break; + case 'g': + // 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:(float)(va_arg(args, double))]; + break; + case 'l': + i++; + if (i < length) { + unichar next = [sql characterAtIndex:i]; + if (next == 'l') { + i++; + if (i < length && [sql characterAtIndex:i] == 'd') { + //%lld + arg = [NSNumber numberWithLongLong:va_arg(args, long long)]; + } + else if (i < length && [sql characterAtIndex:i] == 'u') { + //%llu + arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)]; + } + else { + i--; + } + } + else if (next == 'd') { + //%ld + arg = [NSNumber numberWithLong:va_arg(args, long)]; + } + else if (next == 'u') { + //%lu + arg = [NSNumber numberWithUnsignedLong:va_arg(args, unsigned long)]; + } + else { + i--; + } + } + else { + i--; + } + break; + default: + // something else that we can't interpret. just pass it on through like normal + break; + } + } + else if (current == '%') { + // percent sign; skip this character + add = '\0'; + } + + if (arg != nil) { + [cleanedSQL appendString:@"?"]; + [arguments addObject:arg]; + } + else if (add == (unichar)'@' && last == (unichar) '%') { + [cleanedSQL appendFormat:@"NULL"]; + } + else if (add != '\0') { + [cleanedSQL appendFormat:@"%C", add]; + } + last = current; + } +} + +- (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 (_isExecutingStatement) { + [self warnInUse]; + return 0x00; + } + + _isExecutingStatement = YES; + + int rc = 0x00; + sqlite3_stmt *pStmt = 0x00; + FMStatement *statement = 0x00; + FMResultSet *rs = 0x00; + + if (_traceExecution && sql) { + NSLog(@"%@ executeQuery: %@", self, sql); + } + + if (_shouldCacheStatements) { + statement = [self cachedStatementForQuery:sql]; + pStmt = statement ? [statement statement] : 0x00; + [statement reset]; + } + + int numberOfRetries = 0; + BOOL retry = NO; + + if (!pStmt) { + do { + retry = NO; + 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)) { + NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]); + NSLog(@"Database busy"); + sqlite3_finalize(pStmt); + _isExecutingStatement = NO; + return nil; + } + } + else if (SQLITE_OK != rc) { + + if (_logsErrors) { + NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); + NSLog(@"DB Query: %@", sql); + NSLog(@"DB Path: %@", _databasePath); +#ifndef NS_BLOCK_ASSERTIONS + if (_crashOnErrors) { + abort(); + NSAssert2(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); + } +#endif + } + + sqlite3_finalize(pStmt); + _isExecutingStatement = NO; + return nil; + } + } + while (retry); + } + + id obj; + int idx = 0; + int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!) + + // If dictionaryArgs is passed in, that means we are using sqlite's named parameter support + if (dictionaryArgs) { + + 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]; + // increment the binding count, so our check below works out + idx++; + } + else { + NSLog(@"Could not find index for %@", dictionaryKey); + } + } + } + else { + + while (idx < queryCount) { + + if (arrayArgs && idx < [arrayArgs count]) { + obj = [arrayArgs objectAtIndex:(NSUInteger)idx]; + } + else if (args) { + obj = va_arg(args, id); + } + else { + //We ran out of arguments + break; + } + + if (_traceExecution) { + if ([obj isKindOfClass:[NSData class]]) { + NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]); + } + else { + NSLog(@"obj: %@", obj); + } + } + + 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); + _isExecutingStatement = NO; + return nil; + } + + FMDBRetain(statement); // to balance the release below + + if (!statement) { + statement = [[FMStatement alloc] init]; + [statement setStatement:pStmt]; + + if (_shouldCacheStatements) { + [self setCachedStatement:statement forQuery:sql]; + } + } + + // the statement gets closed in rs's dealloc or [rs close]; + rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self]; + [rs setQuery:sql]; + + NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs]; + [_openResultSets addObject:openResultSet]; + + [statement setUseCount:[statement useCount] + 1]; + + FMDBRelease(statement); + + _isExecutingStatement = NO; + + return rs; +} + +- (FMResultSet *)executeQuery:(NSString*)sql, ... { + va_list args; + va_start(args, sql); + + id result = [self executeQuery:sql withArgumentsInArray:nil orDictionary:nil orVAList:args]; + + va_end(args); + return result; +} + +- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... { + va_list args; + va_start(args, format); + + NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]]; + NSMutableArray *arguments = [NSMutableArray array]; + [self extractSQL:format argumentsList:args intoString:sql arguments:arguments]; + + va_end(args); + + return [self executeQuery:sql withArgumentsInArray:arguments]; +} + +- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments { + return [self executeQuery:sql withArgumentsInArray:arguments orDictionary:nil orVAList:nil]; +} + +- (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args { + + if (![self databaseExists]) { + return NO; + } + + if (_isExecutingStatement) { + [self warnInUse]; + return NO; + } + + _isExecutingStatement = YES; + + int rc = 0x00; + sqlite3_stmt *pStmt = 0x00; + FMStatement *cachedStmt = 0x00; + + if (_traceExecution && sql) { + NSLog(@"%@ executeUpdate: %@", self, sql); + } + + if (_shouldCacheStatements) { + cachedStmt = [self cachedStatementForQuery:sql]; + pStmt = cachedStmt ? [cachedStmt statement] : 0x00; + [cachedStmt reset]; + } + + int numberOfRetries = 0; + BOOL retry = NO; + + if (!pStmt) { + + do { + retry = NO; + 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)) { + NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]); + NSLog(@"Database busy"); + sqlite3_finalize(pStmt); + _isExecutingStatement = NO; + return NO; + } + } + else if (SQLITE_OK != rc) { + + if (_logsErrors) { + NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); + NSLog(@"DB Query: %@", sql); + NSLog(@"DB Path: %@", _databasePath); +#ifndef NS_BLOCK_ASSERTIONS + if (_crashOnErrors) { + abort(); + NSAssert2(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); + } +#endif + } + + sqlite3_finalize(pStmt); + + if (outErr) { + *outErr = [self errorWithMessage:[NSString stringWithUTF8String:sqlite3_errmsg(_db)]]; + } + + _isExecutingStatement = NO; + return NO; + } + } + while (retry); + } + + id obj; + int idx = 0; + int queryCount = sqlite3_bind_parameter_count(pStmt); + + // If dictionaryArgs is passed in, that means we are using sqlite's named parameter support + if (dictionaryArgs) { + + 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]; + + // increment the binding count, so our check below works out + idx++; + } + else { + NSLog(@"Could not find index for %@", dictionaryKey); + } + } + } + else { + + while (idx < queryCount) { + + if (arrayArgs && idx < [arrayArgs count]) { + obj = [arrayArgs objectAtIndex:(NSUInteger)idx]; + } + else if (args) { + obj = va_arg(args, id); + } + else { + //We ran out of arguments + break; + } + + if (_traceExecution) { + if ([obj isKindOfClass:[NSData class]]) { + NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]); + } + else { + NSLog(@"obj: %@", obj); + } + } + + idx++; + + [self bindObject:obj toColumn:idx inStatement:pStmt]; + } + } + + + if (idx != queryCount) { + NSLog(@"Error: the bind count (%d) is not correct for the # of variables in the query (%d) (%@) (executeUpdate)", idx, queryCount, sql); + sqlite3_finalize(pStmt); + _isExecutingStatement = NO; + return NO; + } + + /* Call sqlite3_step() to run the virtual machine. Since the SQL being + ** executed is not a SELECT statement, we assume no data will be returned. + */ + numberOfRetries = 0; + + do { + rc = sqlite3_step(pStmt); + retry = NO; + + 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(pStmt); + if (rc != SQLITE_LOCKED) { + NSLog(@"Unexpected result from sqlite3_reset (%d) eu", rc); + } + } + usleep(20); + + if (_busyRetryTimeout && (numberOfRetries++ > _busyRetryTimeout)) { + NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]); + NSLog(@"Database busy"); + retry = NO; + } + } + 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(@"DB Query: %@", sql); + } + else if (SQLITE_MISUSE == rc) { + // uh oh. + 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(@"DB Query: %@", sql); + } + + } while (retry); + + if (rc == SQLITE_ROW) { + NSAssert1(NO, @"A executeUpdate is being called with a query string '%@'", sql); + } + + if (_shouldCacheStatements && !cachedStmt) { + cachedStmt = [[FMStatement alloc] init]; + + [cachedStmt setStatement:pStmt]; + + [self setCachedStatement:cachedStmt forQuery:sql]; + + FMDBRelease(cachedStmt); + } + + int closeErrorCode; + + if (cachedStmt) { + [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. + */ + closeErrorCode = sqlite3_finalize(pStmt); + } + + if (closeErrorCode != SQLITE_OK) { + NSLog(@"Unknown error finalizing or resetting statement (%d: %s)", closeErrorCode, sqlite3_errmsg(_db)); + NSLog(@"DB Query: %@", sql); + } + + _isExecutingStatement = NO; + return (rc == SQLITE_DONE || rc == SQLITE_OK); +} + + +- (BOOL)executeUpdate:(NSString*)sql, ... { + va_list args; + va_start(args, sql); + + 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 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]]; + NSMutableArray *arguments = [NSMutableArray array]; + + [self extractSQL:format argumentsList:args intoString:sql arguments:arguments]; + + va_end(args); + + return [self executeUpdate:sql withArgumentsInArray:arguments]; +} + +- (BOOL)update:(NSString*)sql withErrorAndBindings:(NSError**)outErr, ... { + va_list args; + va_start(args, outErr); + + 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"]; + + if (b) { + _inTransaction = NO; + } + + return b; +} + +- (BOOL)commit { + BOOL b = [self executeUpdate:@"commit transaction"]; + + if (b) { + _inTransaction = NO; + } + + return b; +} + +- (BOOL)beginDeferredTransaction { + + BOOL b = [self executeUpdate:@"begin deferred transaction"]; + if (b) { + _inTransaction = YES; + } + + return b; +} + +- (BOOL)beginTransaction { + + BOOL b = [self executeUpdate:@"begin exclusive transaction"]; + if (b) { + _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)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; +} + +- (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; +} + +- (void)setShouldCacheStatements:(BOOL)value { + + _shouldCacheStatements = value; + + if (_shouldCacheStatements && !_cachedStatements) { + [self setCachedStatements:[NSMutableDictionary dictionary]]; + } + + if (!_shouldCacheStatements) { + [self setCachedStatements:nil]; + } +} + +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 + + + +@implementation FMStatement +@synthesize statement=_statement; +@synthesize query=_query; +@synthesize useCount=_useCount; + +- (void)finalize { + [self close]; + [super finalize]; +} + +- (void)dealloc { + [self close]; + FMDBRelease(_query); +#if ! __has_feature(objc_arc) + [super dealloc]; +#endif +} + +- (void)close { + if (_statement) { + sqlite3_finalize(_statement); + _statement = 0x00; + } +} + +- (void)reset { + if (_statement) { + sqlite3_reset(_statement); + } +} + +- (NSString*)description { + return [NSString stringWithFormat:@"%@ %ld hit(s) for query %@", [super description], _useCount, _query]; +} + + +@end + diff --git a/media/ios/Other Sources/fmdb/FMDatabaseAdditions.h b/media/ios/Other Sources/fmdb/FMDatabaseAdditions.h new file mode 100755 index 000000000..513c299a8 --- /dev/null +++ b/media/ios/Other Sources/fmdb/FMDatabaseAdditions.h @@ -0,0 +1,47 @@ +// +// FMDatabaseAdditions.h +// fmkit +// +// Created by August Mueller on 10/30/05. +// Copyright 2005 Flying Meat Inc.. All rights reserved. +// + +#import +@interface FMDatabase (FMDatabaseAdditions) + + +- (int)intForQuery:(NSString*)objs, ...; +- (long)longForQuery:(NSString*)objs, ...; +- (BOOL)boolForQuery:(NSString*)objs, ...; +- (double)doubleForQuery:(NSString*)objs, ...; +- (NSString*)stringForQuery:(NSString*)objs, ...; +- (NSData*)dataForQuery:(NSString*)objs, ...; +- (NSDate*)dateForQuery:(NSString*)objs, ...; + +// Notice that there's no dataNoCopyForQuery:. +// That would be a bad idea, because we close out the result set, and then what +// happens to the data that we just didn't copy? Who knows, not I. + + +- (BOOL)tableExists:(NSString*)tableName; +- (FMResultSet*)getSchema; +- (FMResultSet*)getTableSchema:(NSString*)tableName; + +- (BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName; + +- (BOOL)validateSQL:(NSString*)sql error:(NSError**)error; + + +#if SQLITE_VERSION_NUMBER >= 3007017 +- (uint32_t)applicationID; +- (void)setApplicationID:(uint32_t)appID; + +- (NSString*)applicationIDString; +- (void)setApplicationIDString:(NSString*)s; +#endif + + +// deprecated - use columnExists:inTableWithName: instead. +- (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName __attribute__ ((deprecated)); + +@end diff --git a/media/ios/Other Sources/fmdb/FMDatabaseAdditions.m b/media/ios/Other Sources/fmdb/FMDatabaseAdditions.m new file mode 100755 index 000000000..a6fde0ffc --- /dev/null +++ b/media/ios/Other Sources/fmdb/FMDatabaseAdditions.m @@ -0,0 +1,209 @@ +// +// FMDatabaseAdditions.m +// fmkit +// +// Created by August Mueller on 10/30/05. +// Copyright 2005 Flying Meat Inc.. All rights reserved. +// + +#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 orDictionary:0x00 orVAList:args]; \ +va_end(args); \ +if (![resultSet next]) { return (type)0; } \ +type ret = [resultSet sel:0]; \ +[resultSet close]; \ +[resultSet setParentDB:nil]; \ +return ret; + + +- (NSString*)stringForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSString *, stringForColumnIndex); +} + +- (int)intForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(int, intForColumnIndex); +} + +- (long)longForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(long, longForColumnIndex); +} + +- (BOOL)boolForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(BOOL, boolForColumnIndex); +} + +- (double)doubleForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(double, doubleForColumnIndex); +} + +- (NSData*)dataForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSData *, dataForColumnIndex); +} + +- (NSDate*)dateForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSDate *, dateForColumnIndex); +} + + +- (BOOL)tableExists:(NSString*)tableName { + + tableName = [tableName lowercaseString]; + + FMResultSet *rs = [self executeQuery:@"select [sql] from sqlite_master where [type] = 'table' and lower(name) = ?", tableName]; + + //if at least one next exists, table exists + 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) +*/ +- (FMResultSet*)getSchema { + + //result colums: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING] + FMResultSet *rs = [self executeQuery:@"SELECT type, name, tbl_name, rootpage, sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type != 'meta' AND name NOT LIKE 'sqlite_%' ORDER BY tbl_name, type DESC, name"]; + + return rs; +} + +/* + 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] + FMResultSet *rs = [self executeQuery:[NSString stringWithFormat: @"PRAGMA table_info('%@')", tableName]]; + + return rs; +} + +- (BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName { + + BOOL returnBool = NO; + + tableName = [tableName lowercaseString]; + columnName = [columnName lowercaseString]; + + FMResultSet *rs = [self getTableSchema:tableName]; + + //check if column is present in table schema + while ([rs next]) { + if ([[[rs stringForColumn:@"name"] lowercaseString] isEqualToString:columnName]) { + returnBool = YES; + break; + } + } + + //If this is not done FMDatabase instance stays out of pool + [rs close]; + + return returnBool; +} + +#if SQLITE_VERSION_NUMBER >= 3007017 +- (uint32_t)applicationID { + + uint32_t r = 0; + + FMResultSet *rs = [self executeQuery:@"pragma application_id"]; + + if ([rs next]) { + r = (uint32_t)[rs longLongIntForColumnIndex:0]; + } + + [rs close]; + + return r; +} + +- (NSString*)applicationIDString { + NSString *s = NSFileTypeForHFSTypeCode([self applicationID]); + + assert([s length] == 6); + + s = [s substringWithRange:NSMakeRange(1, 4)]; + + + return s; + +} + +- (void)setApplicationID:(uint32_t)appID { + NSString *query = [NSString stringWithFormat:@"PRAGMA application_id=%d", appID]; + FMResultSet *rs = [self executeQuery:query]; + [rs next]; + [rs close]; +} + +- (void)setApplicationIDString:(NSString*)s { + + if ([s length] != 4) { + NSLog(@"setApplicationIDString: string passed is not exactly 4 chars long. (was %ld)", [s length]); + } + + [self setApplicationID:NSHFSTypeCodeFromFileType([NSString stringWithFormat:@"'%@'", s])]; +} + +#endif + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +- (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName __attribute__ ((deprecated)) { + return [self columnExists:columnName inTableWithName:tableName]; +} + +#pragma clang diagnostic pop + +- (BOOL)validateSQL:(NSString*)sql error:(NSError**)error { + sqlite3_stmt *pStmt = NULL; + BOOL validationSucceeded = YES; + BOOL keepTrying = YES; + int numberOfRetries = 0; + + while (keepTrying == YES) { + keepTrying = NO; + 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)) { + NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]); + NSLog(@"Database busy"); + } + } + else if (rc != SQLITE_OK) { + validationSucceeded = NO; + if (error) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:[self lastErrorCode] + userInfo:[NSDictionary dictionaryWithObject:[self lastErrorMessage] + forKey:NSLocalizedDescriptionKey]]; + } + } + } + + sqlite3_finalize(pStmt); + + return validationSucceeded; +} + +@end diff --git a/media/ios/Other Sources/fmdb/FMDatabasePool.h b/media/ios/Other Sources/fmdb/FMDatabasePool.h new file mode 100755 index 000000000..8fe0c3e6e --- /dev/null +++ b/media/ios/Other Sources/fmdb/FMDatabasePool.h @@ -0,0 +1,75 @@ +// +// 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. + +If you really really really know what you're doing and FMDatabasePool is what +you really really need (ie, you're using a read only database), OK you can use +it. But just be careful not to deadlock! + +For an example on deadlocking, search for: +ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD +in the main.m file. + +*/ + + + +@class FMDatabase; + +@interface FMDatabasePool : NSObject { + NSString *_path; + + dispatch_queue_t _lockQueue; + + NSMutableArray *_databaseInPool; + NSMutableArray *_databaseOutPool; + + __unsafe_unretained id _delegate; + + NSUInteger _maximumNumberOfDatabasesToCreate; +} + +@property (atomic, retain) NSString *path; +@property (atomic, assign) id delegate; +@property (atomic, 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/media/ios/Other Sources/fmdb/FMDatabasePool.m b/media/ios/Other Sources/fmdb/FMDatabasePool.m new file mode 100755 index 000000000..4cad6cb48 --- /dev/null +++ b/media/ios/Other Sources/fmdb/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) { + FMDBDispatchQueueRelease(_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 NSUInteger count; + + [self executeLocked:^() { + count = [_databaseInPool count]; + }]; + + return count; +} + +- (NSUInteger)countOfCheckedOutDatabases { + + __block NSUInteger count; + + [self executeLocked:^() { + count = [_databaseOutPool count]; + }]; + + return count; +} + +- (NSUInteger)countOfOpenDatabases { + __block NSUInteger 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/media/ios/Other Sources/fmdb/FMDatabaseQueue.h b/media/ios/Other Sources/fmdb/FMDatabaseQueue.h new file mode 100755 index 000000000..d51452c7c --- /dev/null +++ b/media/ios/Other Sources/fmdb/FMDatabaseQueue.h @@ -0,0 +1,38 @@ +// +// FMDatabaseQueue.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 (atomic, 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/media/ios/Other Sources/fmdb/FMDatabaseQueue.m b/media/ios/Other Sources/fmdb/FMDatabaseQueue.m new file mode 100755 index 000000000..10fabf5a6 --- /dev/null +++ b/media/ios/Other Sources/fmdb/FMDatabaseQueue.m @@ -0,0 +1,176 @@ +// +// FMDatabaseQueue.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) { + FMDBDispatchQueueRelease(_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/media/ios/Other Sources/fmdb/FMResultSet.h b/media/ios/Other Sources/fmdb/FMResultSet.h new file mode 100755 index 000000000..42dd00044 --- /dev/null +++ b/media/ios/Other Sources/fmdb/FMResultSet.h @@ -0,0 +1,104 @@ +#import +#import "sqlite3.h" + +#ifndef __has_feature // Optional. +#define __has_feature(x) 0 // Compatibility with non-clang compilers. +#endif + +#ifndef NS_RETURNS_NOT_RETAINED +#if __has_feature(attribute_ns_returns_not_retained) +#define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained)) +#else +#define NS_RETURNS_NOT_RETAINED +#endif +#endif + +@class FMDatabase; +@class FMStatement; + +@interface FMResultSet : NSObject { + FMDatabase *_parentDB; + FMStatement *_statement; + + NSString *_query; + NSMutableDictionary *_columnNameToIndexMap; +} + +@property (atomic, retain) NSString *query; +@property (readonly) NSMutableDictionary *columnNameToIndexMap; +@property (atomic, retain) FMStatement *statement; + ++ (id)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB; + +- (void)close; + +- (void)setParentDB:(FMDatabase *)newDb; + +- (BOOL)next; +- (BOOL)hasAnotherRow; + +- (int)columnCount; + +- (int)columnIndexForName:(NSString*)columnName; +- (NSString*)columnNameForIndex:(int)columnIdx; + +- (int)intForColumn:(NSString*)columnName; +- (int)intForColumnIndex:(int)columnIdx; + +- (long)longForColumn:(NSString*)columnName; +- (long)longForColumnIndex:(int)columnIdx; + +- (long long int)longLongIntForColumn:(NSString*)columnName; +- (long long int)longLongIntForColumnIndex:(int)columnIdx; + +- (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName; +- (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx; + +- (BOOL)boolForColumn:(NSString*)columnName; +- (BOOL)boolForColumnIndex:(int)columnIdx; + +- (double)doubleForColumn:(NSString*)columnName; +- (double)doubleForColumnIndex:(int)columnIdx; + +- (NSString*)stringForColumn:(NSString*)columnName; +- (NSString*)stringForColumnIndex:(int)columnIdx; + +- (NSDate*)dateForColumn:(NSString*)columnName; +- (NSDate*)dateForColumnIndex:(int)columnIdx; + +- (NSData*)dataForColumn:(NSString*)columnName; +- (NSData*)dataForColumnIndex:(int)columnIdx; + +- (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx; +- (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName; + +// returns one of NSNumber, NSString, NSData, or NSNull +- (id)objectForColumnName:(NSString*)columnName; +- (id)objectForColumnIndex:(int)columnIdx; + +- (id)objectForKeyedSubscript:(NSString *)columnName; +- (id)objectAtIndexedSubscript:(int)columnIdx; + +/* +If you are going to use this data after you iterate over the next row, or after you close the +result set, make sure to make a copy of the data first (or just use dataForColumn:/dataForColumnIndex:) +If you don't, you're going to be in a world of hurt when you try and use the data. +*/ +- (NSData*)dataNoCopyForColumn:(NSString*)columnName NS_RETURNS_NOT_RETAINED; +- (NSData*)dataNoCopyForColumnIndex:(int)columnIdx NS_RETURNS_NOT_RETAINED; + +- (BOOL)columnIndexIsNull:(int)columnIdx; +- (BOOL)columnIsNull:(NSString*)columnName; + + +/* Returns a dictionary of the row results mapped to case sensitive keys of the column names. */ +- (NSDictionary*)resultDictionary; + +/* Please use resultDictionary instead. Also, beware that resultDictionary is case sensitive! */ +- (NSDictionary*)resultDict __attribute__ ((deprecated)); + +- (void)kvcMagic:(id)object; + + +@end + diff --git a/media/ios/Other Sources/fmdb/FMResultSet.m b/media/ios/Other Sources/fmdb/FMResultSet.m new file mode 100755 index 000000000..f9d89b274 --- /dev/null +++ b/media/ios/Other Sources/fmdb/FMResultSet.m @@ -0,0 +1,413 @@ +#import "FMResultSet.h" +#import "FMDatabase.h" +#import "unistd.h" + +@interface FMDatabase () +- (void)resultSetDidClose:(FMResultSet *)resultSet; +@end + + +@implementation FMResultSet +@synthesize query=_query; +@synthesize statement=_statement; + ++ (id)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB { + + FMResultSet *rs = [[FMResultSet alloc] init]; + + [rs setStatement:statement]; + [rs setParentDB:aDB]; + + return FMDBReturnAutoreleased(rs); +} + +- (void)finalize { + [self close]; + [super finalize]; +} + +- (void)dealloc { + [self close]; + + FMDBRelease(_query); + _query = nil; + + FMDBRelease(_columnNameToIndexMap); + _columnNameToIndexMap = nil; + +#if ! __has_feature(objc_arc) + [super dealloc]; +#endif +} + +- (void)close { + [_statement reset]; + FMDBRelease(_statement); + _statement = nil; + + // we don't need this anymore... (i think) + //[_parentDB setInUse:NO]; + [_parentDB resultSetDidClose:self]; + _parentDB = nil; +} + +- (int)columnCount { + return sqlite3_column_count([_statement statement]); +} + +- (NSMutableDictionary *)columnNameToIndexMap { + if (!_columnNameToIndexMap) { + int columnCount = sqlite3_column_count([_statement statement]); + _columnNameToIndexMap = [[NSMutableDictionary alloc] initWithCapacity:(NSUInteger)columnCount]; + int columnIdx = 0; + for (columnIdx = 0; columnIdx < columnCount; columnIdx++) { + [_columnNameToIndexMap setObject:[NSNumber numberWithInt:columnIdx] + forKey:[[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)] lowercaseString]]; + } + } + return _columnNameToIndexMap; +} + +- (void)kvcMagic:(id)object { + + 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); + + // check for a null row + if (c) { + NSString *s = [NSString stringWithUTF8String:c]; + + [object setValue:s forKey:[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]]; + } + } +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +- (NSDictionary*)resultDict { + + NSUInteger num_cols = (NSUInteger)sqlite3_data_count([_statement statement]); + + if (num_cols > 0) { + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols]; + + NSEnumerator *columnNames = [[self columnNameToIndexMap] keyEnumerator]; + NSString *columnName = nil; + while ((columnName = [columnNames nextObject])) { + id objectValue = [self objectForColumnName:columnName]; + [dict setObject:objectValue forKey:columnName]; + } + + return FMDBReturnAutoreleased([dict copy]); + } + else { + NSLog(@"Warning: There seem to be no columns in this set."); + } + + return nil; +} + +#pragma clang diagnostic pop + +- (NSDictionary*)resultDictionary { + + NSUInteger num_cols = (NSUInteger)sqlite3_data_count([_statement statement]); + + if (num_cols > 0) { + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols]; + + int columnCount = sqlite3_column_count([_statement statement]); + + int columnIdx = 0; + for (columnIdx = 0; columnIdx < columnCount; columnIdx++) { + + NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]; + id objectValue = [self objectForColumnIndex:columnIdx]; + [dict setObject:objectValue forKey:columnName]; + } + + return dict; + } + else { + NSLog(@"Warning: There seem to be no columns in this set."); + } + + return nil; +} + + + + + +- (BOOL)next { + + int rc; + BOOL retry; + int numberOfRetries = 0; + do { + retry = NO; + + 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]); + if (rc != SQLITE_LOCKED) { + NSLog(@"Unexpected result from sqlite3_reset (%d) rs", rc); + } + } + usleep(20); + + if ([_parentDB busyRetryTimeout] && (numberOfRetries++ > [_parentDB busyRetryTimeout])) { + + NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [_parentDB databasePath]); + NSLog(@"Database busy"); + break; + } + } + else if (SQLITE_DONE == rc || SQLITE_ROW == rc) { + // all is well, let's return. + } + else if (SQLITE_ERROR == rc) { + 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])); + break; + } + else { + // wtf? + NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle])); + break; + } + + } while (retry); + + + if (rc != SQLITE_ROW) { + [self close]; + } + + return (rc == SQLITE_ROW); +} + +- (BOOL)hasAnotherRow { + return sqlite3_errcode([_parentDB sqliteHandle]) == SQLITE_ROW; +} + +- (int)columnIndexForName:(NSString*)columnName { + columnName = [columnName lowercaseString]; + + NSNumber *n = [[self columnNameToIndexMap] objectForKey:columnName]; + + if (n) { + return [n intValue]; + } + + NSLog(@"Warning: I could not find the column named '%@'.", columnName); + + return -1; +} + + + +- (int)intForColumn:(NSString*)columnName { + return [self intForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (int)intForColumnIndex:(int)columnIdx { + return sqlite3_column_int([_statement statement], columnIdx); +} + +- (long)longForColumn:(NSString*)columnName { + return [self longForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (long)longForColumnIndex:(int)columnIdx { + return (long)sqlite3_column_int64([_statement statement], columnIdx); +} + +- (long long int)longLongIntForColumn:(NSString*)columnName { + return [self longLongIntForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (long long int)longLongIntForColumnIndex:(int)columnIdx { + return sqlite3_column_int64([_statement statement], columnIdx); +} + +- (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName { + return [self unsignedLongLongIntForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx { + return (unsigned long long int)[self longLongIntForColumnIndex:columnIdx]; +} + +- (BOOL)boolForColumn:(NSString*)columnName { + return [self boolForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (BOOL)boolForColumnIndex:(int)columnIdx { + return ([self intForColumnIndex:columnIdx] != 0); +} + +- (double)doubleForColumn:(NSString*)columnName { + return [self doubleForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (double)doubleForColumnIndex:(int)columnIdx { + return sqlite3_column_double([_statement statement], columnIdx); +} + +- (NSString*)stringForColumnIndex:(int)columnIdx { + + if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + return nil; + } + + const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx); + + if (!c) { + // null row. + return nil; + } + + return [NSString stringWithUTF8String:c]; +} + +- (NSString*)stringForColumn:(NSString*)columnName { + return [self stringForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (NSDate*)dateForColumn:(NSString*)columnName { + return [self dateForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (NSDate*)dateForColumnIndex:(int)columnIdx { + + if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + return nil; + } + + return [_parentDB hasDateFormatter] ? [_parentDB dateFromString:[self stringForColumnIndex:columnIdx]] : [NSDate dateWithTimeIntervalSince1970:[self doubleForColumnIndex:columnIdx]]; +} + + +- (NSData*)dataForColumn:(NSString*)columnName { + return [self dataForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (NSData*)dataForColumnIndex:(int)columnIdx { + + if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + return nil; + } + + int dataSize = sqlite3_column_bytes([_statement statement], columnIdx); + + NSMutableData *data = [NSMutableData dataWithLength:(NSUInteger)dataSize]; + + memcpy([data mutableBytes], sqlite3_column_blob([_statement statement], columnIdx), dataSize); + + return data; +} + + +- (NSData*)dataNoCopyForColumn:(NSString*)columnName { + return [self dataNoCopyForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (NSData*)dataNoCopyForColumnIndex:(int)columnIdx { + + if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + return nil; + } + + int dataSize = sqlite3_column_bytes([_statement statement], columnIdx); + + NSData *data = [NSData dataWithBytesNoCopy:(void *)sqlite3_column_blob([_statement statement], columnIdx) length:(NSUInteger)dataSize freeWhenDone:NO]; + + return data; +} + + +- (BOOL)columnIndexIsNull:(int)columnIdx { + return sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL; +} + +- (BOOL)columnIsNull:(NSString*)columnName { + return [self columnIndexIsNull:[self columnIndexForName:columnName]]; +} + +- (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx { + + if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + return nil; + } + + return sqlite3_column_text([_statement statement], columnIdx); +} + +- (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName { + return [self UTF8StringForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (id)objectForColumnIndex:(int)columnIdx { + int columnType = sqlite3_column_type([_statement statement], columnIdx); + + id returnValue = nil; + + if (columnType == SQLITE_INTEGER) { + returnValue = [NSNumber numberWithLongLong:[self longLongIntForColumnIndex:columnIdx]]; + } + else if (columnType == SQLITE_FLOAT) { + returnValue = [NSNumber numberWithDouble:[self doubleForColumnIndex:columnIdx]]; + } + else if (columnType == SQLITE_BLOB) { + returnValue = [self dataForColumnIndex:columnIdx]; + } + else { + //default to a string for everything else + returnValue = [self stringForColumnIndex:columnIdx]; + } + + if (returnValue == nil) { + returnValue = [NSNull null]; + } + + return returnValue; +} + +- (id)objectForColumnName:(NSString*)columnName { + return [self objectForColumnIndex:[self columnIndexForName: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)]; +} + +- (void)setParentDB:(FMDatabase *)newDb { + _parentDB = newDb; +} + +- (id)objectAtIndexedSubscript:(int)columnIdx { + return [self objectForColumnIndex:columnIdx]; +} + +- (id)objectForKeyedSubscript:(NSString *)columnName { + return [self objectForColumnName:columnName]; +} + + +@end diff --git a/media/ios/Other Sources/fmdb/fmdb.m b/media/ios/Other Sources/fmdb/fmdb.m new file mode 100755 index 000000000..b1d6af626 --- /dev/null +++ b/media/ios/Other Sources/fmdb/fmdb.m @@ -0,0 +1,1455 @@ +#import +#import "FMDatabase.h" +#import "FMDatabaseAdditions.h" +#import "FMDatabasePool.h" +#import "FMDatabaseQueue.h" + +#define FMDBQuickCheck(SomeBool) { if (!(SomeBool)) { NSLog(@"Failure on line %d", __LINE__); abort(); } } + +void testPool(NSString *dbPath); +void testDateFormat(); +void FMDBReportABugFunction(); + +int main (int argc, const char * argv[]) { + +@autoreleasepool { + + FMDBReportABugFunction(); + + NSString *dbPath = @"/tmp/tmp.db"; + + // delete the old db. + NSFileManager *fileManager = [NSFileManager defaultManager]; + [fileManager removeItemAtPath:dbPath error:nil]; + + FMDatabase *db = [FMDatabase databaseWithPath:dbPath]; + + 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]); + } + + + if (![db open]) { + NSLog(@"Could not open db."); + + return 0; + } + + // kind of experimentalish. + [db setShouldCacheStatements:YES]; + + // create a bad statement, just to test the error code. + [db executeUpdate:@"blah blah blah"]; + + FMDBQuickCheck([db hadError]); + + if ([db hadError]) { + NSLog(@"Err %d: %@", [db lastErrorCode], [db lastErrorMessage]); + } + + NSError *err = 0x00; + FMDBQuickCheck(![db update:@"blah blah blah" withErrorAndBindings:&err]); + FMDBQuickCheck(err != nil); + FMDBQuickCheck([err code] == SQLITE_ERROR); + NSLog(@"err: '%@'", err); + + + + // empty strings should still return a value. + FMDBQuickCheck(([db boolForQuery:@"SELECT ? not null", @""])); + + // same with empty bits o' mutable data + FMDBQuickCheck(([db boolForQuery:@"SELECT ? not null", [NSMutableData data]])); + + // same with empty bits o' data + FMDBQuickCheck(([db boolForQuery:@"SELECT ? not null", [NSData data]])); + + + + // how do we do pragmas? Like so: + FMResultSet *ps = [db executeQuery:@"PRAGMA journal_mode=delete"]; + FMDBQuickCheck(![db hadError]); + FMDBQuickCheck(ps); + FMDBQuickCheck([ps next]); + [ps close]; + + // oh, but some pragmas require updates? + [db executeUpdate:@"PRAGMA page_size=2048"]; + FMDBQuickCheck(![db hadError]); + + // what about a vacuum? + [db executeUpdate:@"vacuum"]; + FMDBQuickCheck(![db hadError]); + + // but of course, I don't bother checking the error codes below. + // Bad programmer, no cookie. + + [db executeUpdate:@"create table test (a text, b text, c integer, d double, e double)"]; + + + [db beginTransaction]; + int i = 0; + while (i++ < 20) { + [db executeUpdate:@"insert into test (a, b, c, d, e) values (?, ?, ?, ?, ?)" , + @"hi'", // look! I put in a ', and I'm not escaping it! + [NSString stringWithFormat:@"number %d", i], + [NSNumber numberWithInt:i], + [NSDate date], + [NSNumber numberWithFloat:2.2f]]; + } + [db commit]; + + + + // do it again, just because + [db beginTransaction]; + i = 0; + while (i++ < 20) { + [db executeUpdate:@"insert into test (a, b, c, d, e) values (?, ?, ?, ?, ?)" , + @"hi again'", // look! I put in a ', and I'm not escaping it! + [NSString stringWithFormat:@"number %d", i], + [NSNumber numberWithInt:i], + [NSDate date], + [NSNumber numberWithFloat:2.2f]]; + } + [db commit]; + + + + + + FMResultSet *rs = [db executeQuery:@"select rowid,* from test where a = ?", @"hi'"]; + while ([rs next]) { + // just print out what we've got in a number of formats. + NSLog(@"%d %@ %@ %@ %@ %f %f", + [rs intForColumn:@"c"], + [rs stringForColumn:@"b"], + [rs stringForColumn:@"a"], + [rs stringForColumn:@"rowid"], + [rs dateForColumn:@"d"], + [rs doubleForColumn:@"d"], + [rs doubleForColumn:@"e"]); + + + if (!([[rs columnNameForIndex:0] isEqualToString:@"rowid"] && + [[rs columnNameForIndex:1] isEqualToString:@"a"]) + ) { + NSLog(@"WHOA THERE BUDDY, columnNameForIndex ISN'T WORKING!"); + return 7; + } + } + // close the result set. + // it'll also close when it's dealloc'd, but we're closing the database before + // the autorelease pool closes, so sqlite will complain about it. + [rs close]; + + FMDBQuickCheck(![db hasOpenResultSets]); + + + + rs = [db executeQuery:@"select rowid, a, b, c from test"]; + while ([rs next]) { + + FMDBQuickCheck([rs[0] isEqualTo:rs[@"rowid"]]); + FMDBQuickCheck([rs[1] isEqualTo:rs[@"a"]]); + FMDBQuickCheck([rs[2] isEqualTo:rs[@"b"]]); + FMDBQuickCheck([rs[3] isEqualTo:rs[@"c"]]); + } + [rs close]; + + + + + + [db executeUpdate:@"create table ull (a integer)"]; + + [db executeUpdate:@"insert into ull (a) values (?)" , [NSNumber numberWithUnsignedLongLong:ULLONG_MAX]]; + + rs = [db executeQuery:@"select a from ull"]; + while ([rs next]) { + unsigned long long a = [rs unsignedLongLongIntForColumnIndex:0]; + unsigned long long b = [rs unsignedLongLongIntForColumn:@"a"]; + + FMDBQuickCheck(a == ULLONG_MAX); + FMDBQuickCheck(b == ULLONG_MAX); + } + + + // check case sensitive result dictionary. + [db executeUpdate:@"create table cs (aRowName integer, bRowName text)"]; + FMDBQuickCheck(![db hadError]); + [db executeUpdate:@"insert into cs (aRowName, bRowName) values (?, ?)" , [NSNumber numberWithBool:1], @"hello"]; + FMDBQuickCheck(![db hadError]); + + rs = [db executeQuery:@"select * from cs"]; + while ([rs next]) { + NSDictionary *d = [rs resultDictionary]; + + FMDBQuickCheck([d objectForKey:@"aRowName"]); + FMDBQuickCheck(![d objectForKey:@"arowname"]); + FMDBQuickCheck([d objectForKey:@"bRowName"]); + FMDBQuickCheck(![d objectForKey:@"browname"]); + } + + + // check funky table names + getTableSchema + [db executeUpdate:@"create table '234 fds' (foo text)"]; + FMDBQuickCheck(![db hadError]); + rs = [db getTableSchema:@"234 fds"]; + FMDBQuickCheck([rs next]); + [rs close]; + + +#if SQLITE_VERSION_NUMBER >= 3007017 + { + uint32_t appID = NSHFSTypeCodeFromFileType(NSFileTypeForHFSTypeCode('fmdb')); + + [db setApplicationID:appID]; + + uint32_t rAppID = [db applicationID]; + + NSLog(@"rAppID: %d", rAppID); + + FMDBQuickCheck(rAppID == appID); + + [db setApplicationIDString:@"acrn"]; + + NSString *s = [db applicationIDString]; + + NSLog(@"s: '%@'", s); + + FMDBQuickCheck([s isEqualToString:@"acrn"]); + + } + +#endif + + + + + { + // ------------------------------------------------------------------------------- + // Named parameters count test. + FMDBQuickCheck([db executeUpdate:@"create table namedparamcounttest (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 namedparamcounttest values (:a, :b, :c, :d)" withParameterDictionary:dictionaryArgs]); + + rs = [db executeQuery:@"select * from namedparamcounttest"]; + + 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]; + + // note that at this point, dictionaryArgs has way more values than we need, but the query should still work since + // a is in there, and that's all we need. + rs = [db executeQuery:@"select * from namedparamcounttest where a = :a" withParameterDictionary:dictionaryArgs]; + + FMDBQuickCheck((rs != nil)); + FMDBQuickCheck([rs next]); + [rs close]; + + + + // ***** Please note the following codes ***** + + dictionaryArgs = [NSMutableDictionary dictionary]; + + [dictionaryArgs setObject:@"NewText1" forKey:@"a"]; + [dictionaryArgs setObject:@"NewText2" forKey:@"b"]; + [dictionaryArgs setObject:@"OneMoreText" forKey:@"OneMore"]; + + BOOL rc = [db executeUpdate:@"update namedparamcounttest set a = :a, b = :b where b = 'Text2'" withParameterDictionary:dictionaryArgs]; + + FMDBQuickCheck(rc); + + if (!rc) { + NSLog(@"ERROR: %d - %@", db.lastErrorCode, db.lastErrorMessage); + } + + + } + + + + + + + + + + + // ---------------------------------------------------------------------------------------- + // blob support. + [db executeUpdate:@"create table blobTable (a text, b blob)"]; + + // let's read in an image from safari's app bundle. + NSData *safariCompass = [NSData dataWithContentsOfFile:@"/Applications/Safari.app/Contents/Resources/compass.icns"]; + if (safariCompass) { + [db executeUpdate:@"insert into blobTable (a, b) values (?,?)", @"safari's compass", safariCompass]; + + rs = [db executeQuery:@"select b from blobTable where a = ?", @"safari's compass"]; + if ([rs next]) { + safariCompass = [rs dataForColumn:@"b"]; + [safariCompass writeToFile:@"/tmp/compass.icns" atomically:NO]; + + // let's look at our fancy image that we just wrote out.. + system("/usr/bin/open /tmp/compass.icns"); + + // ye shall read the header for this function, or suffer the consequences. + safariCompass = [rs dataNoCopyForColumn:@"b"]; + [safariCompass writeToFile:@"/tmp/compass_data_no_copy.icns" atomically:NO]; + system("/usr/bin/open /tmp/compass_data_no_copy.icns"); + } + else { + NSLog(@"Could not select image."); + } + + [rs close]; + + } + else { + NSLog(@"Can't find compass image.."); + } + + + // test out the convenience methods in +Additions + [db executeUpdate:@"create table t1 (a integer)"]; + [db executeUpdate:@"insert into t1 values (?)", [NSNumber numberWithInt:5]]; + + NSLog(@"Count of changes (should be 1): %d", [db changes]); + FMDBQuickCheck([db changes] == 1); + + int ia = [db intForQuery:@"select a from t1 where a = ?", [NSNumber numberWithInt:5]]; + if (ia != 5) { + NSLog(@"intForQuery didn't work (a != 5)"); + } + + // test the busy rety timeout schtuff. + + [db setBusyRetryTimeout:500]; + + FMDatabase *newDb = [FMDatabase databaseWithPath:dbPath]; + [newDb open]; + + rs = [newDb executeQuery:@"select rowid,* from test where a = ?", @"hi'"]; + [rs next]; // just grab one... which will keep the db locked. + + NSLog(@"Testing the busy timeout"); + + BOOL success = [db executeUpdate:@"insert into t1 values (5)"]; + + if (success) { + NSLog(@"Whoa- the database didn't stay locked!"); + return 7; + } + else { + NSLog(@"Hurray, our timeout worked"); + } + + [rs close]; + [newDb close]; + + success = [db executeUpdate:@"insert into t1 values (5)"]; + if (!success) { + NSLog(@"Whoa- the database shouldn't be locked!"); + return 8; + } + else { + NSLog(@"Hurray, we can insert again!"); + } + + + + // test some nullness. + [db executeUpdate:@"create table t2 (a integer, b integer)"]; + + if (![db executeUpdate:@"insert into t2 values (?, ?)", nil, [NSNumber numberWithInt:5]]) { + NSLog(@"UH OH, can't insert a nil value for some reason..."); + } + + + + + rs = [db executeQuery:@"select * from t2"]; + while ([rs next]) { + NSString *aa = [rs stringForColumnIndex:0]; + NSString *b = [rs stringForColumnIndex:1]; + + if (aa != nil) { + NSLog(@"%s:%d", __FUNCTION__, __LINE__); + NSLog(@"OH OH, PROBLEMO!"); + return 10; + } + else { + NSLog(@"YAY, NULL VALUES"); + } + + if (![b isEqualToString:@"5"]) { + NSLog(@"%s:%d", __FUNCTION__, __LINE__); + NSLog(@"OH OH, PROBLEMO!"); + return 10; + } + } + + + + + + + + + + + // test some inner loop funkness. + [db executeUpdate:@"create table t3 (a somevalue)"]; + + + // do it again, just because + [db beginTransaction]; + i = 0; + while (i++ < 20) { + [db executeUpdate:@"insert into t3 (a) values (?)" , [NSNumber numberWithInt:i]]; + } + [db commit]; + + + + + rs = [db executeQuery:@"select * from t3"]; + while ([rs next]) { + int foo = [rs intForColumnIndex:0]; + + int newVal = foo + 100; + + [db executeUpdate:@"update t3 set a = ? where a = ?" , [NSNumber numberWithInt:newVal], [NSNumber numberWithInt:foo]]; + + + FMResultSet *rs2 = [db executeQuery:@"select a from t3 where a = ?", [NSNumber numberWithInt:newVal]]; + [rs2 next]; + + if ([rs2 intForColumnIndex:0] != newVal) { + NSLog(@"Oh crap, our update didn't work out!"); + return 9; + } + + [rs2 close]; + } + + + // NSNull tests + [db executeUpdate:@"create table nulltest (a text, b text)"]; + + [db executeUpdate:@"insert into nulltest (a, b) values (?, ?)" , [NSNull null], @"a"]; + [db executeUpdate:@"insert into nulltest (a, b) values (?, ?)" , nil, @"b"]; + + rs = [db executeQuery:@"select * from nulltest"]; + + while ([rs next]) { + + NSString *a = [rs stringForColumnIndex:0]; + NSString *b = [rs stringForColumnIndex:1]; + + if (!b) { + NSLog(@"Oh crap, the nil / null inserts didn't work!"); + return 10; + } + + if (a) { + NSLog(@"Oh crap, the nil / null inserts didn't work (son of error message)!"); + return 11; + } + else { + NSLog(@"HURRAH FOR NSNULL (and nil)!"); + } + } + + + FMDBQuickCheck([db columnExists:@"a" inTableWithName:@"nulltest"]); + FMDBQuickCheck([db columnExists:@"b" inTableWithName:@"nulltest"]); + FMDBQuickCheck(![db columnExists:@"c" inTableWithName:@"nulltest"]); + + + // null dates + + NSDate *date = [NSDate date]; + [db executeUpdate:@"create table datetest (a double, b double, c double)"]; + [db executeUpdate:@"insert into datetest (a, b, c) values (?, ?, 0)" , [NSNull null], date]; + + rs = [db executeQuery:@"select * from datetest"]; + + while ([rs next]) { + + NSDate *a = [rs dateForColumnIndex:0]; + NSDate *b = [rs dateForColumnIndex:1]; + NSDate *c = [rs dateForColumnIndex:2]; + + if (a) { + NSLog(@"Oh crap, the null date insert didn't work!"); + return 12; + } + + if (!c) { + NSLog(@"Oh crap, the 0 date insert didn't work!"); + return 12; + } + + NSTimeInterval dti = fabs([b timeIntervalSinceDate:date]); + + if (floor(dti) > 0.0) { + NSLog(@"Date matches didn't really happen... time difference of %f", dti); + return 13; + } + + + dti = fabs([c timeIntervalSinceDate:[NSDate dateWithTimeIntervalSince1970:0]]); + + if (floor(dti) > 0.0) { + NSLog(@"Date matches didn't really happen... time difference of %f", dti); + return 13; + } + } + + NSDate *foo = [db dateForQuery:@"select b from datetest where c = 0"]; + assert(foo); + NSTimeInterval dti = fabs([foo timeIntervalSinceDate:date]); + if (floor(dti) > 0.0) { + NSLog(@"Date matches didn't really happen... time difference of %f", dti); + return 14; + } + + [db executeUpdate:@"create table nulltest2 (s text, d data, i integer, f double, b integer)"]; + + // grab the data for this again, since we overwrote it with some memory that has since disapeared. + safariCompass = [NSData dataWithContentsOfFile:@"/Applications/Safari.app/Contents/Resources/compass.icns"]; + + [db executeUpdate:@"insert into nulltest2 (s, d, i, f, b) values (?, ?, ?, ?, ?)" , @"Hi", safariCompass, [NSNumber numberWithInt:12], [NSNumber numberWithFloat:4.4f], [NSNumber numberWithBool:YES]]; + [db executeUpdate:@"insert into nulltest2 (s, d, i, f, b) values (?, ?, ?, ?, ?)" , nil, nil, nil, nil, [NSNull null]]; + + rs = [db executeQuery:@"select * from nulltest2"]; + + while ([rs next]) { + + i = [rs intForColumnIndex:2]; + + if (i == 12) { + // it's the first row we inserted. + FMDBQuickCheck(![rs columnIndexIsNull:0]); + FMDBQuickCheck(![rs columnIndexIsNull:1]); + FMDBQuickCheck(![rs columnIndexIsNull:2]); + FMDBQuickCheck(![rs columnIndexIsNull:3]); + FMDBQuickCheck(![rs columnIndexIsNull:4]); + FMDBQuickCheck( [rs columnIndexIsNull:5]); + + FMDBQuickCheck([[rs dataForColumn:@"d"] length] == [safariCompass length]); + FMDBQuickCheck(![rs dataForColumn:@"notthere"]); + FMDBQuickCheck(![rs stringForColumnIndex:-2]); + FMDBQuickCheck([rs boolForColumnIndex:4]); + FMDBQuickCheck([rs boolForColumn:@"b"]); + + FMDBQuickCheck(fabs(4.4 - [rs doubleForColumn:@"f"]) < 0.0000001); + + FMDBQuickCheck(12 == [rs intForColumn:@"i"]); + FMDBQuickCheck(12 == [rs intForColumnIndex:2]); + + FMDBQuickCheck(0 == [rs intForColumnIndex:12]); // there is no 12 + FMDBQuickCheck(0 == [rs intForColumn:@"notthere"]); + + FMDBQuickCheck(12 == [rs longForColumn:@"i"]); + FMDBQuickCheck(12 == [rs longLongIntForColumn:@"i"]); + } + else { + // let's test various null things. + + FMDBQuickCheck([rs columnIndexIsNull:0]); + FMDBQuickCheck([rs columnIndexIsNull:1]); + FMDBQuickCheck([rs columnIndexIsNull:2]); + FMDBQuickCheck([rs columnIndexIsNull:3]); + FMDBQuickCheck([rs columnIndexIsNull:4]); + FMDBQuickCheck([rs columnIndexIsNull:5]); + + + FMDBQuickCheck(![rs dataForColumn:@"d"]); + + } + } + + + + { + + [db executeUpdate:@"create table utest (a text)"]; + [db executeUpdate:@"insert into utest values (?)", @"/übertest"]; + + rs = [db executeQuery:@"select * from utest where a = ?", @"/übertest"]; + FMDBQuickCheck([rs next]); + [rs close]; + } + + + { + [db executeUpdate:@"create table testOneHundredTwelvePointTwo (a text, b integer)"]; + [db executeUpdate:@"insert into testOneHundredTwelvePointTwo values (?, ?)" withArgumentsInArray:[NSArray arrayWithObjects:@"one", [NSNumber numberWithInteger:2], nil]]; + [db executeUpdate:@"insert into testOneHundredTwelvePointTwo values (?, ?)" withArgumentsInArray:[NSArray arrayWithObjects:@"one", [NSNumber numberWithInteger:3], nil]]; + + + rs = [db executeQuery:@"select * from testOneHundredTwelvePointTwo where b > ?" withArgumentsInArray:[NSArray arrayWithObject:[NSNumber numberWithInteger:1]]]; + + FMDBQuickCheck([rs next]); + + FMDBQuickCheck([rs hasAnotherRow]); + FMDBQuickCheck(![db hadError]); + + FMDBQuickCheck([[rs stringForColumnIndex:0] isEqualToString:@"one"]); + FMDBQuickCheck([rs intForColumnIndex:1] == 2); + + FMDBQuickCheck([rs next]); + + FMDBQuickCheck([rs intForColumnIndex:1] == 3); + + FMDBQuickCheck(![rs next]); + FMDBQuickCheck(![rs hasAnotherRow]); + + } + + { + + FMDBQuickCheck([db executeUpdate:@"create table t4 (a text, b text)"]); + FMDBQuickCheck(([db executeUpdate:@"insert into t4 (a, b) values (?, ?)", @"one", @"two"])); + + rs = [db executeQuery:@"select t4.a as 't4.a', t4.b from t4;"]; + + FMDBQuickCheck((rs != nil)); + + [rs next]; + + FMDBQuickCheck([[rs stringForColumn:@"t4.a"] isEqualToString:@"one"]); + FMDBQuickCheck([[rs stringForColumn:@"b"] isEqualToString:@"two"]); + + FMDBQuickCheck(strcmp((const char*)[rs UTF8StringForColumnName:@"b"], "two") == 0); + + [rs close]; + + // let's try these again, with the withArgumentsInArray: variation + FMDBQuickCheck([db executeUpdate:@"drop table t4;" withArgumentsInArray:[NSArray array]]); + FMDBQuickCheck([db executeUpdate:@"create table t4 (a text, b text)" withArgumentsInArray:[NSArray array]]); + FMDBQuickCheck(([db executeUpdate:@"insert into t4 (a, b) values (?, ?)" withArgumentsInArray:[NSArray arrayWithObjects:@"one", @"two", nil]])); + + rs = [db executeQuery:@"select t4.a as 't4.a', t4.b from t4;" withArgumentsInArray:[NSArray array]]; + + FMDBQuickCheck((rs != nil)); + + [rs next]; + + FMDBQuickCheck([[rs stringForColumn:@"t4.a"] isEqualToString:@"one"]); + FMDBQuickCheck([[rs stringForColumn:@"b"] isEqualToString:@"two"]); + + FMDBQuickCheck(strcmp((const char*)[rs UTF8StringForColumnName:@"b"], "two") == 0); + + [rs close]; + } + + + + + { + FMDBQuickCheck([db tableExists:@"t4"]); + FMDBQuickCheck(![db tableExists:@"thisdoesntexist"]); + + rs = [db getSchema]; + while ([rs next]) { + FMDBQuickCheck([[rs stringForColumn:@"type"] isEqualToString:@"table"]); + } + } + + + { + FMDBQuickCheck([db executeUpdate:@"create table t5 (a text, b int, c blob, d text, e text)"]); + FMDBQuickCheck(([db executeUpdateWithFormat:@"insert into t5 values (%s, %d, %@, %c, %lld)", "text", 42, @"BLOB", 'd', 12345678901234])); + + rs = [db executeQueryWithFormat:@"select * from t5 where a = %s and a = %@ and b = %d", "text", @"text", 42]; + FMDBQuickCheck((rs != nil)); + + [rs next]; + + FMDBQuickCheck([[rs stringForColumn:@"a"] isEqualToString:@"text"]); + FMDBQuickCheck(([rs intForColumn:@"b"] == 42)); + 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]; + + } + + { + FMDBQuickCheck([db executeUpdate:@"create table tatwhat (a text)"]); + + BOOL worked = [db executeUpdateWithFormat:@"insert into tatwhat values(%@)", nil]; + + FMDBQuickCheck(worked); + + rs = [db executeQueryWithFormat:@"select * from tatwhat"]; + FMDBQuickCheck((rs != nil)); + FMDBQuickCheck(([rs next])); + FMDBQuickCheck([rs columnIndexIsNull:0]); + + FMDBQuickCheck((![rs next])); + + } + + + { + FMDBQuickCheck(([db update:@"insert into t5 values (?, ?, ?, ?, ?)" withErrorAndBindings:&err, @"text", [NSNumber numberWithInt:42], @"BLOB", @"d", [NSNumber numberWithInt:0]])); + + } + + { + rs = [db executeQuery:@"select * from t5 where a=?" withArgumentsInArray:@[]]; + FMDBQuickCheck((![rs next])); + } + + // test attach for the heck of it. + { + + //FMDatabase *dbA = [FMDatabase databaseWithPath:dbPath]; + [fileManager removeItemAtPath:@"/tmp/attachme.db" error:nil]; + FMDatabase *dbB = [FMDatabase databaseWithPath:@"/tmp/attachme.db"]; + FMDBQuickCheck([dbB open]); + FMDBQuickCheck([dbB executeUpdate:@"create table attached (a text)"]); + FMDBQuickCheck(([dbB executeUpdate:@"insert into attached values (?)", @"test"])); + FMDBQuickCheck([dbB close]); + + [db executeUpdate:@"attach database '/tmp/attachme.db' as attack"]; + + rs = [db executeQuery:@"select * from attack.attached"]; + FMDBQuickCheck([rs next]); + [rs close]; + + } + + + + { + // ------------------------------------------------------------------------------- + // 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]) { + NSString *file = [rs stringForColumn:@"file"]; + NSLog(@"database_list: %@", file); + } + + + // print out some stats if we are using cached statements. + if ([db shouldCacheStatements]) { + + NSEnumerator *e = [[db cachedStatements] objectEnumerator];; + FMStatement *statement; + + while ((statement = [e nextObject])) { + NSLog(@"%@", statement); + } + } + + + [db setShouldCacheStatements:true]; + + [db executeUpdate:@"CREATE TABLE testCacheStatements(key INTEGER PRIMARY KEY, value INTEGER)"]; + [db executeUpdate:@"INSERT INTO testCacheStatements (key, value) VALUES (1, 2)"]; + [db executeUpdate:@"INSERT INTO testCacheStatements (key, value) VALUES (2, 4)"]; + + FMDBQuickCheck([[db executeQuery:@"SELECT * FROM testCacheStatements WHERE key=1"] next]); + FMDBQuickCheck([[db executeQuery:@"SELECT * FROM testCacheStatements WHERE key=1"] next]); + + [db close]; + + + testPool(dbPath); + testDateFormat(); + + + + FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:dbPath]; + + FMDBQuickCheck(queue); + + { + [queue inDatabase:^(FMDatabase *adb) { + + + + [adb executeUpdate:@"create table qfoo (foo text)"]; + [adb executeUpdate:@"insert into qfoo values ('hi')"]; + [adb executeUpdate:@"insert into qfoo values ('hello')"]; + [adb executeUpdate:@"insert into qfoo values ('not')"]; + + + + int count = 0; + FMResultSet *rsl = [adb executeQuery:@"select * from qfoo where foo like 'h%'"]; + while ([rsl next]) { + count++; + } + + FMDBQuickCheck(count == 2); + + count = 0; + rsl = [adb 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: + size_t ops = 16; + + dispatch_queue_t dqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + + 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 *adb, BOOL *rollback) { + NSLog(@"Starting query %ld", nby); + + FMResultSet *rsl = [adb 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 *adb, BOOL *rollback) { + NSLog(@"Starting update %ld", nby); + [adb executeUpdate:@"insert into qfoo values ('1')"]; + [adb executeUpdate:@"insert into qfoo values ('2')"]; + [adb executeUpdate:@"insert into qfoo values ('3')"]; + NSLog(@"Ending update %ld", nby); + }]; + }); + + [queue close]; + + [queue inDatabase:^(FMDatabase *adb) { + FMDBQuickCheck([adb executeUpdate:@"insert into qfoo values ('1')"]); + }]; + } + + { + + + [queue inDatabase:^(FMDatabase *adb) { + [adb executeUpdate:@"create table colNameTest (a, b, c, d)"]; + FMDBQuickCheck([adb executeUpdate:@"insert into colNameTest values (1, 2, 3, 4)"]); + + FMResultSet *ars = [adb executeQuery:@"select * from colNameTest"]; + + NSDictionary *d = [ars columnNameToIndexMap]; + FMDBQuickCheck([d count] == 4); + + FMDBQuickCheck([[d objectForKey:@"a"] intValue] == 0); + FMDBQuickCheck([[d objectForKey:@"b"] intValue] == 1); + FMDBQuickCheck([[d objectForKey:@"c"] intValue] == 2); + FMDBQuickCheck([[d objectForKey:@"d"] intValue] == 3); + + [ars close]; + + }]; + + } + + + { + [queue inDatabase:^(FMDatabase *adb) { + [adb executeUpdate:@"create table transtest (a integer)"]; + FMDBQuickCheck([adb executeUpdate:@"insert into transtest values (1)"]); + FMDBQuickCheck([adb executeUpdate:@"insert into transtest values (2)"]); + + int rowCount = 0; + FMResultSet *ars = [adb executeQuery:@"select * from transtest"]; + while ([ars next]) { + rowCount++; + } + + FMDBQuickCheck(rowCount == 2); + }]; + + + + [queue inTransaction:^(FMDatabase *adb, BOOL *rollback) { + FMDBQuickCheck([adb executeUpdate:@"insert into transtest values (3)"]); + + if (YES) { + // uh oh!, something went wrong (not really, this is just a test + *rollback = YES; + return; + } + + FMDBQuickCheck([adb executeUpdate:@"insert into transtest values (4)"]); + }]; + + [queue inDatabase:^(FMDatabase *adb) { + + int rowCount = 0; + FMResultSet *ars = [adb executeQuery:@"select * from transtest"]; + while ([ars next]) { + rowCount++; + } + + FMDBQuickCheck(![adb hasOpenResultSets]); + + NSLog(@"after rollback, rowCount is %d (should be 2)", rowCount); + + FMDBQuickCheck(rowCount == 2); + }]; + } + + // hey, let's make a custom function! + + [queue inDatabase:^(FMDatabase *adb) { + + [adb executeUpdate:@"create table ftest (foo text)"]; + [adb executeUpdate:@"insert into ftest values ('hello')"]; + [adb executeUpdate:@"insert into ftest values ('hi')"]; + [adb executeUpdate:@"insert into ftest values ('not h!')"]; + [adb executeUpdate:@"insert into ftest values ('definitely not h!')"]; + + [adb makeFunctionNamed:@"StringStartsWithH" maximumArguments:1 withBlock:^(sqlite3_context *context, int aargc, sqlite3_value **aargv) { + if (sqlite3_value_type(aargv[0]) == SQLITE_TEXT) { + + @autoreleasepool { + + const char *c = (const char *)sqlite3_value_text(aargv[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(aargv[0]), __FUNCTION__, __LINE__); + sqlite3_result_null(context); + } + }]; + + int rowCount = 0; + FMResultSet *ars = [adb executeQuery:@"select * from ftest where StringStartsWithH(foo)"]; + while ([ars 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 *arollback) { + FMDBQuickCheck(([adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1010]])); + *arollback = 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); + + }]; + } + + + { + + size_t ops = 128; + + dispatch_queue_t dqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + + 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(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + + 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 + +} + + +/* + Test the date format + */ + +void testOneDateFormat( FMDatabase *db, NSDate *testDate ) { + [db executeUpdate:@"DROP TABLE IF EXISTS test_format"]; + [db executeUpdate:@"CREATE TABLE test_format ( test TEXT )"]; + [db executeUpdate:@"INSERT INTO test_format(test) VALUES (?)", testDate]; + FMResultSet *rs = [db executeQuery:@"SELECT test FROM test_format"]; + if ([rs next]) { + NSDate *found = [rs dateForColumnIndex:0]; + if (NSOrderedSame != [testDate compare:found]) { + NSLog(@"Did not get back what we stored."); + } + } + else { + NSLog(@"Insertion borked"); + } + [rs close]; +} + +void testDateFormat() { + + FMDatabase *db = [FMDatabase databaseWithPath:nil]; // use in-memory DB + [db open]; + + NSDateFormatter *fmt = [FMDatabase storeableDateFormat:@"yyyy-MM-dd HH:mm:ss"]; + + NSDate *testDate = [fmt dateFromString:@"2013-02-20 12:00:00"]; + + // test timestamp dates (ensuring our change does not break those) + testOneDateFormat(db,testDate); + + // now test the string-based timestamp + [db setDateFormat:fmt]; + testOneDateFormat(db, testDate); + + [db close]; +} + + +/* + What is this function for? Think of it as a template which a developer can use + to report bugs. + + If you have a bug, make it reproduce in this function and then let the + developer(s) know either via the github bug reporter or the mailing list. + */ + +void FMDBReportABugFunction() { + + NSString *dbPath = @"/tmp/bugreportsample.db"; + + // delete the old db if it exists + NSFileManager *fileManager = [NSFileManager defaultManager]; + [fileManager removeItemAtPath:dbPath error:nil]; + + FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:dbPath]; + + [queue inDatabase:^(FMDatabase *db) { + + /* + Change the contents of this block to suit your needs. + */ + + BOOL worked = [db executeUpdate:@"create table test (a text, b text, c integer, d double, e double)"]; + FMDBQuickCheck(worked); + + + worked = [db executeUpdate:@"insert into test values ('a', 'b', 1, 2.2, 2.3)"]; + FMDBQuickCheck(worked); + + FMResultSet *rs = [db executeQuery:@"select * from test"]; + FMDBQuickCheck([rs next]); + [rs close]; + + }]; + + + [queue close]; + + + // uncomment the following line if you don't want to run through all the other tests. + //exit(0); + +} + + + + +