diff options
author | ccgus <gus@flyingmeat.com> | 2013-12-10 22:16:53 +0400 |
---|---|---|
committer | ccgus <gus@flyingmeat.com> | 2013-12-10 22:16:53 +0400 |
commit | 8d2dac4001b73f71ce7290757a6bb7c967bd9053 (patch) | |
tree | df98d4c83c55c370668b03a25bd93c0704e2641f | |
parent | 2224f216e79096f92a8ce33b432bd8d75153afe2 (diff) | |
parent | dd7da6f0a471c0777f94a2220eb97b925c06e24a (diff) |
Merge branch 'master' of github.com:ccgus/fmdb into busyTimeout
-rw-r--r-- | .travis.yml | 5 | ||||
-rw-r--r-- | README.markdown | 2 | ||||
-rw-r--r-- | Tests/FMDBTempDBTests.h | 24 | ||||
-rw-r--r-- | Tests/FMDBTempDBTests.m | 63 | ||||
-rw-r--r-- | Tests/FMDatabaseAdditionsTests.m | 97 | ||||
-rw-r--r-- | Tests/FMDatabasePoolTests.m | 293 | ||||
-rw-r--r-- | Tests/FMDatabaseQueueTests.m | 176 | ||||
-rw-r--r-- | Tests/FMDatabaseTests.m | 817 | ||||
-rw-r--r-- | Tests/Schemes/Tests.xcscheme | 83 | ||||
-rw-r--r-- | Tests/Tests-Info.plist | 22 | ||||
-rw-r--r-- | Tests/Tests-Prefix.pch | 11 | ||||
-rw-r--r-- | Tests/en.lproj/InfoPlist.strings | 2 | ||||
-rw-r--r-- | fmdb.xcodeproj/project.pbxproj | 227 | ||||
-rw-r--r-- | src/FMDatabase.h | 27 | ||||
-rw-r--r-- | src/FMDatabasePool.h | 22 | ||||
-rw-r--r-- | src/FMDatabasePool.m | 23 | ||||
-rw-r--r-- | src/FMDatabaseQueue.h | 11 | ||||
-rw-r--r-- | src/FMDatabaseQueue.m | 7 | ||||
-rw-r--r-- | src/fmdb.m | 14 |
19 files changed, 1901 insertions, 25 deletions
diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7e21267 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: objective-c +xcode_project: fmdb.xcodeproj +xcode_scheme: Tests +before_install: + - mkdir -p "fmdb.xcodeproj/xcshareddata/xcschemes" && cp Tests/Schemes/*.xcscheme "fmdb.xcodeproj/xcshareddata/xcschemes/" diff --git a/README.markdown b/README.markdown index 99a40f1..6d5b85d 100644 --- a/README.markdown +++ b/README.markdown @@ -1,6 +1,8 @@ # FMDB This is an Objective-C wrapper around SQLite: http://sqlite.org/ +[![Build Status](https://travis-ci.org/ccgus/fmdb.png?branch=master)](https://travis-ci.org/ccgus/fmdb) + ## The FMDB Mailing List: http://groups.google.com/group/fmdb diff --git a/Tests/FMDBTempDBTests.h b/Tests/FMDBTempDBTests.h new file mode 100644 index 0000000..c84ab8f --- /dev/null +++ b/Tests/FMDBTempDBTests.h @@ -0,0 +1,24 @@ +// +// FMDBTempDBTests.h +// fmdb +// +// Created by Graham Dennis on 24/11/2013. +// +// + +#import <XCTest/XCTest.h> +#import "FMDatabase.h" + +@protocol FMDBTempDBTests <NSObject> + +@optional ++ (void)populateDatabase:(FMDatabase *)database; + +@end + +@interface FMDBTempDBTests : XCTestCase <FMDBTempDBTests> + +@property FMDatabase *db; +@property (readonly) NSString *databasePath; + +@end diff --git a/Tests/FMDBTempDBTests.m b/Tests/FMDBTempDBTests.m new file mode 100644 index 0000000..bb759df --- /dev/null +++ b/Tests/FMDBTempDBTests.m @@ -0,0 +1,63 @@ +// +// FMDBTempDBTests.m +// fmdb +// +// Created by Graham Dennis on 24/11/2013. +// +// + +#import "FMDBTempDBTests.h" + +static NSString *const testDatabasePath = @"/tmp/tmp.db"; +static NSString *const populatedDatabasePath = @"/tmp/tmp-populated.db"; + +@implementation FMDBTempDBTests + ++ (void)setUp +{ + [super setUp]; + + // Delete old populated database + NSFileManager *fileManager = [NSFileManager defaultManager]; + [fileManager removeItemAtPath:populatedDatabasePath error:NULL]; + + if ([self respondsToSelector:@selector(populateDatabase:)]) { + FMDatabase *db = [FMDatabase databaseWithPath:populatedDatabasePath]; + + [db open]; + [self populateDatabase:db]; + [db close]; + } +} + +- (void)setUp +{ + [super setUp]; + + // Delete the old database + NSFileManager *fileManager = [NSFileManager defaultManager]; + [fileManager removeItemAtPath:testDatabasePath error:NULL]; + + if ([[self class] respondsToSelector:@selector(populateDatabase:)]) { + [fileManager copyItemAtPath:populatedDatabasePath toPath:testDatabasePath error:NULL]; + } + + self.db = [FMDatabase databaseWithPath:testDatabasePath]; + + XCTAssertTrue([self.db open], @"Wasn't able to open database"); + [self.db setShouldCacheStatements:YES]; +} + +- (void)tearDown +{ + [super tearDown]; + + [self.db close]; +} + +- (NSString *)databasePath +{ + return testDatabasePath; +} + +@end diff --git a/Tests/FMDatabaseAdditionsTests.m b/Tests/FMDatabaseAdditionsTests.m new file mode 100644 index 0000000..6164847 --- /dev/null +++ b/Tests/FMDatabaseAdditionsTests.m @@ -0,0 +1,97 @@ +// +// FMDatabaseAdditionsTests.m +// fmdb +// +// Created by Graham Dennis on 24/11/2013. +// +// + +#import <XCTest/XCTest.h> +#import "FMDatabaseAdditions.h" + +@interface FMDatabaseAdditionsTests : FMDBTempDBTests + +@end + +@implementation FMDatabaseAdditionsTests + +- (void)setUp +{ + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown +{ + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testFunkyTableNames +{ + [self.db executeUpdate:@"create table '234 fds' (foo text)"]; + XCTAssertFalse([self.db hadError], @"table creation should have succeeded"); + FMResultSet *rs = [self.db getTableSchema:@"234 fds"]; + XCTAssertTrue([rs next], @"Schema should have succeded"); + [rs close]; + XCTAssertFalse([self.db hadError], @"There shouldn't be any errors"); +} + +- (void)testBoolForQuery +{ + BOOL result = [self.db boolForQuery:@"SELECT ? not null", @""]; + XCTAssertTrue(result, @"Empty strings should be considered true"); + + result = [self.db boolForQuery:@"SELECT ? not null", [NSMutableData data]]; + XCTAssertTrue(result, @"Empty mutable data should be considered true"); + + result = [self.db boolForQuery:@"SELECT ? not null", [NSData data]]; + XCTAssertTrue(result, @"Empty data should be considered true"); +} + + +- (void)testIntForQuery +{ + [self.db executeUpdate:@"create table t1 (a integer)"]; + [self.db executeUpdate:@"insert into t1 values (?)", [NSNumber numberWithInt:5]]; + + XCTAssertEqual([self.db changes], 1, @"There should only be one change"); + + int ia = [self.db intForQuery:@"select a from t1 where a = ?", [NSNumber numberWithInt:5]]; + XCTAssertEqual(ia, 5, @"foo"); +} + +- (void)testDateForQuery +{ + NSDate *date = [NSDate date]; + [self.db executeUpdate:@"create table datetest (a double, b double, c double)"]; + [self.db executeUpdate:@"insert into datetest (a, b, c) values (?, ?, 0)" , [NSNull null], date]; + + NSDate *foo = [self.db dateForQuery:@"select b from datetest where c = 0"]; + XCTAssertEqualWithAccuracy([foo timeIntervalSinceDate:date], 0.0, 1.0, @"Dates should be the same to within a second"); +} + +- (void)testTableExists +{ + XCTAssertTrue([self.db executeUpdate:@"create table t4 (a text, b text)"]); + + XCTAssertTrue([self.db tableExists:@"t4"]); + XCTAssertFalse([self.db tableExists:@"thisdoesntexist"]); + + FMResultSet *rs = [self.db getSchema]; + while ([rs next]) { + XCTAssertEqualObjects([rs stringForColumn:@"type"], @"table"); + } + +} + +- (void)testColumnExists +{ + [self.db executeUpdate:@"create table nulltest (a text, b text)"]; + + XCTAssertTrue([self.db columnExists:@"a" inTableWithName:@"nulltest"]); + XCTAssertTrue([self.db columnExists:@"b" inTableWithName:@"nulltest"]); + XCTAssertFalse([self.db columnExists:@"c" inTableWithName:@"nulltest"]); +} + +@end diff --git a/Tests/FMDatabasePoolTests.m b/Tests/FMDatabasePoolTests.m new file mode 100644 index 0000000..36183c5 --- /dev/null +++ b/Tests/FMDatabasePoolTests.m @@ -0,0 +1,293 @@ +// +// FMDatabasePoolTests.m +// fmdb +// +// Created by Graham Dennis on 24/11/2013. +// +// + +#import <XCTest/XCTest.h> + +@interface FMDatabasePoolTests : FMDBTempDBTests + +@property FMDatabasePool *pool; + +@end + +@implementation FMDatabasePoolTests + ++ (void)populateDatabase:(FMDatabase *)db +{ + [db executeUpdate:@"create table easy (a text)"]; + [db executeUpdate:@"create table easy2 (a text)"]; + + [db executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1001]]; + [db executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1002]]; + [db executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1003]]; + + [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')"]; +} + +- (void)setUp +{ + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. + + self.pool = [FMDatabasePool databasePoolWithPath:self.databasePath]; +} + +- (void)tearDown +{ + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testPoolIsInitiallyEmpty +{ + XCTAssertEqual([self.pool countOfOpenDatabases], (NSUInteger)0, @"Pool should be empty on creation"); +} + +- (void)testDatabaseCreation +{ + __block FMDatabase *db1; + + [self.pool inDatabase:^(FMDatabase *db) { + + XCTAssertEqual([self.pool countOfOpenDatabases], (NSUInteger)1, @"Should only have one database at this point"); + + db1 = db; + + }]; + + [self.pool inDatabase:^(FMDatabase *db) { + XCTAssertEqualObjects(db, db1, @"We should get the same database back because there was no need to create a new one"); + + [self.pool inDatabase:^(FMDatabase *db2) { + XCTAssertNotEqualObjects(db2, db, @"We should get a different database because the first was in use."); + }]; + + }]; + + XCTAssertEqual([self.pool countOfOpenDatabases], (NSUInteger)2); + + [self.pool releaseAllDatabases]; + + XCTAssertEqual([self.pool countOfOpenDatabases], (NSUInteger)0, @"We should be back to zero databases again"); +} + +- (void)testCheckedInCheckoutOutCount +{ + [self.pool inDatabase:^(FMDatabase *aDb) { + + XCTAssertEqual([self.pool countOfCheckedInDatabases], (NSUInteger)0); + XCTAssertEqual([self.pool countOfCheckedOutDatabases], (NSUInteger)1); + + XCTAssertTrue(([aDb executeUpdate:@"insert into easy (a) values (?)", @"hi"])); + + // just for fun. + FMResultSet *rs = [aDb executeQuery:@"select * from easy"]; + XCTAssertNotNil(rs); + XCTAssertTrue([rs next]); + while ([rs next]) { ; } // whatevers. + + XCTAssertEqual([self.pool countOfOpenDatabases], (NSUInteger)1); + XCTAssertEqual([self.pool countOfCheckedInDatabases], (NSUInteger)0); + XCTAssertEqual([self.pool countOfCheckedOutDatabases], (NSUInteger)1); + }]; + + XCTAssertEqual([self.pool countOfOpenDatabases], (NSUInteger)1); +} + +- (void)testMaximumDatabaseLimit +{ + [self.pool setMaximumNumberOfDatabasesToCreate:2]; + + [self.pool inDatabase:^(FMDatabase *db) { + [self.pool inDatabase:^(FMDatabase *db2) { + [self.pool inDatabase:^(FMDatabase *db3) { + XCTAssertEqual([self.pool countOfOpenDatabases], (NSUInteger)2); + XCTAssertNil(db3, @"The third database must be nil because we have a maximum of 2 databases in the pool"); + }]; + + }]; + }]; +} + +- (void)testTransaction +{ + [self.pool 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]]; + + XCTAssertEqual([self.pool countOfOpenDatabases], (NSUInteger)1); + XCTAssertEqual([self.pool countOfCheckedInDatabases], (NSUInteger)0); + XCTAssertEqual([self.pool countOfCheckedOutDatabases], (NSUInteger)1); + }]; + + XCTAssertEqual([self.pool countOfOpenDatabases], (NSUInteger)1); + XCTAssertEqual([self.pool countOfCheckedInDatabases], (NSUInteger)1); + XCTAssertEqual([self.pool countOfCheckedOutDatabases], (NSUInteger)0); +} + +- (void)testSelect +{ + [self.pool inDatabase:^(FMDatabase *db) { + FMResultSet *rs = [db executeQuery:@"select * from easy where a = ?", [NSNumber numberWithInt:1001]]; + XCTAssertNotNil(rs); + XCTAssertTrue ([rs next]); + XCTAssertFalse([rs next]); + }]; +} + +- (void)testTransactionRollback +{ + [self.pool inDeferredTransaction:^(FMDatabase *adb, BOOL *rollback) { + XCTAssertTrue(([adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1004]])); + XCTAssertTrue(([adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1005]])); + XCTAssertTrue([[adb executeQuery:@"select * from easy where a == '1004'"] next], @"1004 should be in database"); + + *rollback = YES; + }]; + + [self.pool inDatabase:^(FMDatabase *db) { + XCTAssertFalse([[db executeQuery:@"select * from easy where a == '1004'"] next], @"1004 should not be in database"); + }]; + + XCTAssertEqual([self.pool countOfOpenDatabases], (NSUInteger)1); + XCTAssertEqual([self.pool countOfCheckedInDatabases], (NSUInteger)1); + XCTAssertEqual([self.pool countOfCheckedOutDatabases], (NSUInteger)0); +} + +- (void)testSavepoint +{ + NSError *err = [self.pool inSavePoint:^(FMDatabase *db, BOOL *rollback) { + [db executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1006]]; + }]; + + XCTAssertNil(err); +} + +- (void)testNestedSavepointRollback +{ + NSError *err = [self.pool inSavePoint:^(FMDatabase *adb, BOOL *rollback) { + XCTAssertFalse([adb hadError]); + XCTAssertTrue(([adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1009]])); + + [adb inSavePoint:^(BOOL *arollback) { + XCTAssertTrue(([adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1010]])); + *arollback = YES; + }]; + }]; + + + XCTAssertNil(err); + + [self.pool inDatabase:^(FMDatabase *db) { + FMResultSet *rs = [db executeQuery:@"select * from easy where a = ?", [NSNumber numberWithInt:1009]]; + XCTAssertTrue ([rs next]); + XCTAssertFalse([rs next]); // close it out. + + rs = [db executeQuery:@"select * from easy where a = ?", [NSNumber numberWithInt:1010]]; + XCTAssertFalse([rs next]); + }]; +} + +- (void)testLikeStringQuery +{ + [self.pool inDatabase:^(FMDatabase *db) { + int count = 0; + FMResultSet *rsl = [db executeQuery:@"select * from likefoo where foo like 'h%'"]; + while ([rsl next]) { + count++; + } + + XCTAssertEqual(count, 2); + + count = 0; + rsl = [db executeQuery:@"select * from likefoo where foo like ?", @"h%"]; + while ([rsl next]) { + count++; + } + + XCTAssertEqual(count, 2); + + }]; +} + +- (void)testStressTest +{ + 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:.001]; + } + + [self.pool inDatabase:^(FMDatabase *db) { + FMResultSet *rsl = [db executeQuery:@"select * from likefoo where foo like 'h%'"]; + XCTAssertNotNil(rsl); + int i = 0; + while ([rsl next]) { + i++; + if (nby % 3 == 1) { + [NSThread sleepForTimeInterval:.0005]; + } + } + XCTAssertEqual(i, 2); + }]; + }); + + XCTAssert([self.pool countOfOpenDatabases] < 64, @"There should be significantly less than 64 databases after that stress test"); +} + +- (void)testReadWriteStressTest +{ + 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:.01]; + + [self.pool inTransaction:^(FMDatabase *db, BOOL *rollback) { + FMResultSet *rsl = [db executeQuery:@"select * from likefoo where foo like 'h%'"]; + XCTAssertNotNil(rsl); + while ([rsl next]) { + ;// whatever. + } + + }]; + + } + + if (nby % 3 == 1) { + [NSThread sleepForTimeInterval:.01]; + } + + [self.pool inTransaction:^(FMDatabase *db, BOOL *rollback) { + XCTAssertTrue([db executeUpdate:@"insert into likefoo values ('1')"]); + XCTAssertTrue([db executeUpdate:@"insert into likefoo values ('2')"]); + XCTAssertTrue([db executeUpdate:@"insert into likefoo values ('3')"]); + }]; + }); + + [self.pool releaseAllDatabases]; + + [self.pool inDatabase:^(FMDatabase *db) { + XCTAssertTrue([db executeUpdate:@"insert into likefoo values ('1')"]); + }]; +} + +@end diff --git a/Tests/FMDatabaseQueueTests.m b/Tests/FMDatabaseQueueTests.m new file mode 100644 index 0000000..cd7fb92 --- /dev/null +++ b/Tests/FMDatabaseQueueTests.m @@ -0,0 +1,176 @@ +// +// FMDatabaseQueueTests.m +// fmdb +// +// Created by Graham Dennis on 24/11/2013. +// +// + +#import <XCTest/XCTest.h> +#import "FMDatabaseQueue.h" + +@interface FMDatabaseQueueTests : FMDBTempDBTests + +@property FMDatabaseQueue *queue; + +@end + +@implementation FMDatabaseQueueTests + ++ (void)populateDatabase:(FMDatabase *)db +{ + [db executeUpdate:@"create table easy (a text)"]; + + [db executeUpdate:@"create table qfoo (foo text)"]; + [db executeUpdate:@"insert into qfoo values ('hi')"]; + [db executeUpdate:@"insert into qfoo values ('hello')"]; + [db executeUpdate:@"insert into qfoo values ('not')"]; +} + +- (void)setUp +{ + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. + + self.queue = [FMDatabaseQueue databaseQueueWithPath:self.databasePath]; +} + +- (void)tearDown +{ + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testQueueSelect +{ + [self.queue inDatabase:^(FMDatabase *adb) { + int count = 0; + FMResultSet *rsl = [adb executeQuery:@"select * from qfoo where foo like 'h%'"]; + while ([rsl next]) { + count++; + } + + XCTAssertEqual(count, 2); + + count = 0; + rsl = [adb executeQuery:@"select * from qfoo where foo like ?", @"h%"]; + while ([rsl next]) { + count++; + } + + XCTAssertEqual(count, 2); + }]; +} + +- (void)testReadOnlyQueue +{ + FMDatabaseQueue *queue2 = [FMDatabaseQueue databaseQueueWithPath:self.databasePath flags:SQLITE_OPEN_READONLY]; + XCTAssertNotNil(queue2); + + { + [queue2 inDatabase:^(FMDatabase *db2) { + FMResultSet *rs1 = [db2 executeQuery:@"SELECT * FROM qfoo"]; + XCTAssertNotNil(rs1); + + [rs1 close]; + + XCTAssertFalse(([db2 executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:3]]), @"Insert should fail because this is a read-only database"); + }]; + + [queue2 close]; + + // Check that when we re-open the database, it's still read-only + [queue2 inDatabase:^(FMDatabase *db2) { + FMResultSet *rs1 = [db2 executeQuery:@"SELECT * FROM qfoo"]; + XCTAssertNotNil(rs1); + + [rs1 close]; + + XCTAssertFalse(([db2 executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:3]]), @"Insert should fail because this is a read-only database"); + }]; + } +} + +- (void)testStressTest +{ + 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:.01]; + + [self.queue inTransaction:^(FMDatabase *adb, BOOL *rollback) { + FMResultSet *rsl = [adb executeQuery:@"select * from qfoo where foo like 'h%'"]; + while ([rsl next]) { + ;// whatever. + } + }]; + + } + + if (nby % 3 == 1) { + [NSThread sleepForTimeInterval:.01]; + } + + [self.queue inTransaction:^(FMDatabase *adb, BOOL *rollback) { + XCTAssertTrue([adb executeUpdate:@"insert into qfoo values ('1')"]); + XCTAssertTrue([adb executeUpdate:@"insert into qfoo values ('2')"]); + XCTAssertTrue([adb executeUpdate:@"insert into qfoo values ('3')"]); + }]; + }); + + [self.queue close]; + + [self.queue inDatabase:^(FMDatabase *adb) { + XCTAssertTrue([adb executeUpdate:@"insert into qfoo values ('1')"]); + }]; +} + +- (void)testTransaction +{ + [self.queue inDatabase:^(FMDatabase *adb) { + [adb executeUpdate:@"create table transtest (a integer)"]; + XCTAssertTrue([adb executeUpdate:@"insert into transtest values (1)"]); + XCTAssertTrue([adb executeUpdate:@"insert into transtest values (2)"]); + + int rowCount = 0; + FMResultSet *ars = [adb executeQuery:@"select * from transtest"]; + while ([ars next]) { + rowCount++; + } + + XCTAssertEqual(rowCount, 2); + }]; + + [self.queue inTransaction:^(FMDatabase *adb, BOOL *rollback) { + XCTAssertTrue([adb executeUpdate:@"insert into transtest values (3)"]); + + if (YES) { + // uh oh!, something went wrong (not really, this is just a test + *rollback = YES; + return; + } + + XCTFail(@"This shouldn't be reached"); + }]; + + [self.queue inDatabase:^(FMDatabase *adb) { + + int rowCount = 0; + FMResultSet *ars = [adb executeQuery:@"select * from transtest"]; + while ([ars next]) { + rowCount++; + } + + XCTAssertFalse([adb hasOpenResultSets]); + + XCTAssertEqual(rowCount, 2); + }]; + +} + +@end diff --git a/Tests/FMDatabaseTests.m b/Tests/FMDatabaseTests.m new file mode 100644 index 0000000..f3ef197 --- /dev/null +++ b/Tests/FMDatabaseTests.m @@ -0,0 +1,817 @@ +// +// Tests.m +// Tests +// +// Created by Graham Dennis on 24/11/2013. +// +// + +#import "FMDBTempDBTests.h" +#import "FMDatabase.h" + +@interface FMDatabaseTests : FMDBTempDBTests + +@end + +@implementation FMDatabaseTests + ++ (void)populateDatabase:(FMDatabase *)db +{ + [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]; + + [db executeUpdate:@"create table t3 (a somevalue)"]; + + [db beginTransaction]; + for (int i=0; i < 20; i++) { + [db executeUpdate:@"insert into t3 (a) values (?)", [NSNumber numberWithInt:i]]; + } + [db commit]; +} + +- (void)setUp +{ + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. + +} + +- (void)tearDown +{ + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; + +} + +- (void)testFailOnUnopenedDatabase +{ + [self.db close]; + + XCTAssertNil([self.db executeQuery:@"select * from table"], @"Shouldn't get results from an empty table"); + XCTAssertTrue([self.db hadError], @"Should have failed"); +} + +- (void)testFailOnBadStatement +{ + XCTAssertFalse([self.db executeUpdate:@"blah blah blah"], @"Invalid statement should fail"); + XCTAssertTrue([self.db hadError], @"Should have failed"); +} + +- (void)testFailOnBadStatementWithError +{ + NSError *error = nil; + XCTAssertFalse([self.db update:@"blah blah blah" withErrorAndBindings:&error], @"Invalid statement should fail"); + XCTAssertNotNil(error, @"Should have a non-nil NSError"); + XCTAssertEqual([error code], (NSInteger)SQLITE_ERROR, @"Error should be SQLITE_ERROR"); +} + +- (void)testPragmaJournalMode +{ + FMResultSet *ps = [self.db executeQuery:@"PRAGMA journal_mode=delete"]; + XCTAssertFalse([self.db hadError], @"PRAGMA should have succeeded"); + XCTAssertNotNil(ps, @"Result set should be non-nil"); + XCTAssertTrue([ps next], @"Result set should have a next result"); + [ps close]; +} + +- (void)testPragmaPageSize +{ + [self.db executeUpdate:@"PRAGMA page_size=2048"]; + XCTAssertFalse([self.db hadError], @"PRAGMA should have succeeded"); +} + +- (void)testVacuum +{ + [self.db executeUpdate:@"VACUUM"]; + XCTAssertFalse([self.db hadError], @"VACUUM should have succeeded"); +} + +- (void)testSelectULL +{ + // Unsigned long long + [self.db executeUpdate:@"create table ull (a integer)"]; + + [self.db executeUpdate:@"insert into ull (a) values (?)", [NSNumber numberWithUnsignedLongLong:ULLONG_MAX]]; + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); + + FMResultSet *rs = [self.db executeQuery:@"select a from ull"]; + while ([rs next]) { + XCTAssertEqual([rs unsignedLongLongIntForColumnIndex:0], ULLONG_MAX, @"Result should be ULLONG_MAX"); + XCTAssertEqual([rs unsignedLongLongIntForColumn:@"a"], ULLONG_MAX, @"Result should be ULLONG_MAX"); + } + + [rs close]; + XCTAssertFalse([self.db hasOpenResultSets], @"Shouldn't have any open result sets"); + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); +} + +- (void)testSelectByColumnName +{ + FMResultSet *rs = [self.db executeQuery:@"select rowid,* from test where a = ?", @"hi"]; + + XCTAssertNotNil(rs, @"Should have a non-nil result set"); + + while ([rs next]) { + [rs intForColumn:@"c"]; + XCTAssertNotNil([rs stringForColumn:@"b"], @"Should have non-nil string for 'b'"); + XCTAssertNotNil([rs stringForColumn:@"a"], @"Should have non-nil string for 'a'"); + XCTAssertNotNil([rs stringForColumn:@"rowid"], @"Should have non-nil string for 'rowid'"); + XCTAssertNotNil([rs dateForColumn:@"d"], @"Should have non-nil date for 'd'"); + [rs doubleForColumn:@"d"]; + [rs doubleForColumn:@"e"]; + + XCTAssertEqualObjects([rs columnNameForIndex:0], @"rowid", @"Wrong column name for result set column number"); + XCTAssertEqualObjects([rs columnNameForIndex:1], @"a", @"Wrong column name for result set column number"); + } + + [rs close]; + XCTAssertFalse([self.db hasOpenResultSets], @"Shouldn't have any open result sets"); + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); +} + +- (void)testSelectWithIndexedAndKeyedSubscript +{ + FMResultSet *rs = [self.db executeQuery:@"select rowid, a, b, c from test"]; + + XCTAssertNotNil(rs, @"Should have a non-nil result set"); + + while ([rs next]) { + XCTAssertEqualObjects(rs[0], rs[@"rowid"], @"Column zero should be equal to 'rowid'"); + XCTAssertEqualObjects(rs[1], rs[@"a"], @"Column 1 should be equal to 'a'"); + XCTAssertEqualObjects(rs[2], rs[@"b"], @"Column 2 should be equal to 'b'"); + XCTAssertEqualObjects(rs[3], rs[@"c"], @"Column 3 should be equal to 'c'"); + } + + [rs close]; + XCTAssertFalse([self.db hasOpenResultSets], @"Shouldn't have any open result sets"); + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); +} + +- (void)testBusyRetryTimeout +{ + [self.db executeUpdate:@"create table t1 (a integer)"]; + [self.db executeUpdate:@"insert into t1 values (?)", [NSNumber numberWithInt:5]]; + + [self.db setBusyRetryTimeout:50]; + + FMDatabase *newDB = [FMDatabase databaseWithPath:self.databasePath]; + [newDB open]; + + FMResultSet *rs = [newDB executeQuery:@"select rowid,* from test where a = ?", @"hi'"]; + [rs next]; // just grab one... which will keep the db locked + + XCTAssertFalse([self.db executeUpdate:@"insert into t1 values (5)"], @"Insert should fail because the db is locked by a read"); + XCTAssertEqual([self.db lastErrorCode], SQLITE_BUSY, @"SQLITE_BUSY should be the last error"); + + [rs close]; + [newDB close]; + + XCTAssertTrue([self.db executeUpdate:@"insert into t1 values (5)"], @"The database shouldn't be locked at this point"); +} + +- (void)testCaseSensitiveResultDictionary +{ + // case sensitive result dictionary test + [self.db executeUpdate:@"create table cs (aRowName integer, bRowName text)"]; + [self.db executeUpdate:@"insert into cs (aRowName, bRowName) values (?, ?)", [NSNumber numberWithBool:1], @"hello"]; + + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); + + FMResultSet *rs = [self.db executeQuery:@"select * from cs"]; + while ([rs next]) { + NSDictionary *d = [rs resultDictionary]; + + XCTAssertNotNil([d objectForKey:@"aRowName"], @"aRowName should be non-nil"); + XCTAssertNil([d objectForKey:@"arowname"], @"arowname should be nil"); + XCTAssertNotNil([d objectForKey:@"bRowName"], @"bRowName should be non-nil"); + XCTAssertNil([d objectForKey:@"browname"], @"browname should be nil"); + } + + [rs close]; + XCTAssertFalse([self.db hasOpenResultSets], @"Shouldn't have any open result sets"); + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); +} + +- (void)testNamedParametersCount +{ + XCTAssertTrue([self.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"]; + XCTAssertTrue([self.db executeUpdate:@"insert into namedparamcounttest values (:a, :b, :c, :d)" withParameterDictionary:dictionaryArgs]); + + FMResultSet *rs = [self.db executeQuery:@"select * from namedparamcounttest"]; + + XCTAssertNotNil(rs); + + [rs next]; + + XCTAssertEqualObjects([rs stringForColumn:@"a"], @"Text1"); + XCTAssertEqualObjects([rs stringForColumn:@"b"], @"Text2"); + XCTAssertEqual([rs intForColumn:@"c"], 1); + XCTAssertEqual([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 = [self.db executeQuery:@"select * from namedparamcounttest where a = :a" withParameterDictionary:dictionaryArgs]; + + XCTAssertNotNil(rs); + XCTAssertTrue([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"]; + + XCTAssertTrue([self.db executeUpdate:@"update namedparamcounttest set a = :a, b = :b where b = 'Text2'" withParameterDictionary:dictionaryArgs]); + +} + +- (void)testBlobs +{ + [self.db executeUpdate:@"create table blobTable (a text, b blob)"]; + + // let's read an image from safari's app bundle. + NSData *safariCompass = [NSData dataWithContentsOfFile:@"/Applications/Safari.app/Contents/Resources/compass.icns"]; + if (safariCompass) { + [self.db executeUpdate:@"insert into blobTable (a, b) values (?, ?)", @"safari's compass", safariCompass]; + + FMResultSet *rs = [self.db executeQuery:@"select b from blobTable where a = ?", @"safari's compass"]; + XCTAssertTrue([rs next]); + NSData *readData = [rs dataForColumn:@"b"]; + XCTAssertEqualObjects(readData, safariCompass); + + // ye shall read the header for this function, or suffer the consequences. + NSData *readDataNoCopy = [rs dataNoCopyForColumn:@"b"]; + XCTAssertEqualObjects(readDataNoCopy, safariCompass); + + [rs close]; + XCTAssertFalse([self.db hasOpenResultSets], @"Shouldn't have any open result sets"); + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); + } +} + +- (void)testNullValues +{ + [self.db executeUpdate:@"create table t2 (a integer, b integer)"]; + + BOOL result = [self.db executeUpdate:@"insert into t2 values (?, ?)", nil, [NSNumber numberWithInt:5]]; + XCTAssertTrue(result, @"Failed to insert a nil value"); + + FMResultSet *rs = [self.db executeQuery:@"select * from t2"]; + while ([rs next]) { + XCTAssertNil([rs stringForColumnIndex:0], @"Wasn't able to retrieve a null string"); + XCTAssertEqualObjects([rs stringForColumnIndex:1], @"5"); + } + + [rs close]; + XCTAssertFalse([self.db hasOpenResultSets], @"Shouldn't have any open result sets"); + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); +} + +- (void)testNestedResultSets +{ + FMResultSet *rs = [self.db executeQuery:@"select * from t3"]; + while ([rs next]) { + int foo = [rs intForColumnIndex:0]; + + int newVal = foo + 100; + + [self.db executeUpdate:@"update t3 set a = ? where a = ?", [NSNumber numberWithInt:newVal], [NSNumber numberWithInt:foo]]; + + FMResultSet *rs2 = [self.db executeQuery:@"select a from t3 where a = ?", [NSNumber numberWithInt:newVal]]; + [rs2 next]; + + XCTAssertEqual([rs2 intForColumnIndex:0], newVal); + + [rs2 close]; + } + [rs close]; + XCTAssertFalse([self.db hasOpenResultSets], @"Shouldn't have any open result sets"); + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); +} + +- (void)testNSNullInsertion +{ + [self.db executeUpdate:@"create table nulltest (a text, b text)"]; + + [self.db executeUpdate:@"insert into nulltest (a, b) values (?, ?)", [NSNull null], @"a"]; + [self.db executeUpdate:@"insert into nulltest (a, b) values (?, ?)", nil, @"b"]; + + FMResultSet *rs = [self.db executeQuery:@"select * from nulltest"]; + + while ([rs next]) { + XCTAssertNil([rs stringForColumnIndex:0]); + XCTAssertNotNil([rs stringForColumnIndex:1]); + } + + [rs close]; + + XCTAssertFalse([self.db hasOpenResultSets], @"Shouldn't have any open result sets"); + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); +} + +- (void)testNullDates +{ + NSDate *date = [NSDate date]; + [self.db executeUpdate:@"create table datetest (a double, b double, c double)"]; + [self.db executeUpdate:@"insert into datetest (a, b, c) values (?, ?, 0)" , [NSNull null], date]; + + FMResultSet *rs = [self.db executeQuery:@"select * from datetest"]; + + XCTAssertNotNil(rs); + + while ([rs next]) { + + NSDate *b = [rs dateForColumnIndex:1]; + NSDate *c = [rs dateForColumnIndex:2]; + + XCTAssertNil([rs dateForColumnIndex:0]); + XCTAssertNotNil(c, @"zero date shouldn't be nil"); + + XCTAssertEqualWithAccuracy([b timeIntervalSinceDate:date], 0.0, 1.0, @"Dates should be the same to within a second"); + XCTAssertEqualWithAccuracy([c timeIntervalSince1970], 0.0, 1.0, @"Dates should be the same to within a second"); + } + [rs close]; + + XCTAssertFalse([self.db hasOpenResultSets], @"Shouldn't have any open result sets"); + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); +} + +- (void)testLotsOfNULLs +{ + NSData *safariCompass = [NSData dataWithContentsOfFile:@"/Applications/Safari.app/Contents/Resources/compass.icns"]; + + if (!safariCompass) + return; + + [self.db executeUpdate:@"create table nulltest2 (s text, d data, i integer, f double, b integer)"]; + + [self.db executeUpdate:@"insert into nulltest2 (s, d, i, f, b) values (?, ?, ?, ?, ?)" , @"Hi", safariCompass, [NSNumber numberWithInt:12], [NSNumber numberWithFloat:4.4f], [NSNumber numberWithBool:YES]]; + [self.db executeUpdate:@"insert into nulltest2 (s, d, i, f, b) values (?, ?, ?, ?, ?)" , nil, nil, nil, nil, [NSNull null]]; + + FMResultSet *rs = [self.db executeQuery:@"select * from nulltest2"]; + + while ([rs next]) { + + int i = [rs intForColumnIndex:2]; + + if (i == 12) { + // it's the first row we inserted. + XCTAssertFalse([rs columnIndexIsNull:0]); + XCTAssertFalse([rs columnIndexIsNull:1]); + XCTAssertFalse([rs columnIndexIsNull:2]); + XCTAssertFalse([rs columnIndexIsNull:3]); + XCTAssertFalse([rs columnIndexIsNull:4]); + XCTAssertTrue( [rs columnIndexIsNull:5]); + + XCTAssertEqualObjects([rs dataForColumn:@"d"], safariCompass); + XCTAssertNil([rs dataForColumn:@"notthere"]); + XCTAssertNil([rs stringForColumnIndex:-2], @"Negative columns should return nil results"); + XCTAssertTrue([rs boolForColumnIndex:4]); + XCTAssertTrue([rs boolForColumn:@"b"]); + + XCTAssertEqualWithAccuracy(4.4, [rs doubleForColumn:@"f"], 0.0000001, @"Saving a float and returning it as a double shouldn't change the result much"); + + XCTAssertEqual([rs intForColumn:@"i"], 12); + XCTAssertEqual([rs intForColumnIndex:2], 12); + + XCTAssertEqual([rs intForColumnIndex:12], 0, @"Non-existent columns should return zero for ints"); + XCTAssertEqual([rs intForColumn:@"notthere"], 0, @"Non-existent columns should return zero for ints"); + + XCTAssertEqual([rs longForColumn:@"i"], 12l); + XCTAssertEqual([rs longLongIntForColumn:@"i"], 12ll); + } + else { + // let's test various null things. + + XCTAssertTrue([rs columnIndexIsNull:0]); + XCTAssertTrue([rs columnIndexIsNull:1]); + XCTAssertTrue([rs columnIndexIsNull:2]); + XCTAssertTrue([rs columnIndexIsNull:3]); + XCTAssertTrue([rs columnIndexIsNull:4]); + XCTAssertTrue([rs columnIndexIsNull:5]); + + + XCTAssertNil([rs dataForColumn:@"d"]); + } + } + + [rs close]; + XCTAssertFalse([self.db hasOpenResultSets], @"Shouldn't have any open result sets"); + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); +} + +- (void)testUTF8Strings +{ + [self.db executeUpdate:@"create table utest (a text)"]; + [self.db executeUpdate:@"insert into utest values (?)", @"/übertest"]; + + FMResultSet *rs = [self.db executeQuery:@"select * from utest where a = ?", @"/übertest"]; + XCTAssertTrue([rs next]); + [rs close]; + XCTAssertFalse([self.db hasOpenResultSets], @"Shouldn't have any open result sets"); + XCTAssertFalse([self.db hadError], @"Shouldn't have any errors"); +} + +- (void)testArgumentsInArray +{ + [self.db executeUpdate:@"create table testOneHundredTwelvePointTwo (a text, b integer)"]; + [self.db executeUpdate:@"insert into testOneHundredTwelvePointTwo values (?, ?)" withArgumentsInArray:[NSArray arrayWithObjects:@"one", [NSNumber numberWithInteger:2], nil]]; + [self.db executeUpdate:@"insert into testOneHundredTwelvePointTwo values (?, ?)" withArgumentsInArray:[NSArray arrayWithObjects:@"one", [NSNumber numberWithInteger:3], nil]]; + + + FMResultSet *rs = [self.db executeQuery:@"select * from testOneHundredTwelvePointTwo where b > ?" withArgumentsInArray:[NSArray arrayWithObject:[NSNumber numberWithInteger:1]]]; + + XCTAssertTrue([rs next]); + + XCTAssertTrue([rs hasAnotherRow]); + XCTAssertFalse([self.db hadError]); + + XCTAssertEqualObjects([rs stringForColumnIndex:0], @"one"); + XCTAssertEqual([rs intForColumnIndex:1], 2); + + XCTAssertTrue([rs next]); + + XCTAssertEqual([rs intForColumnIndex:1], 3); + + XCTAssertFalse([rs next]); + XCTAssertFalse([rs hasAnotherRow]); +} + +- (void)testColumnNamesContainingPeriods +{ + XCTAssertTrue([self.db executeUpdate:@"create table t4 (a text, b text)"]); + [self.db executeUpdate:@"insert into t4 (a, b) values (?, ?)", @"one", @"two"]; + + FMResultSet *rs = [self.db executeQuery:@"select t4.a as 't4.a', t4.b from t4;"]; + + XCTAssertNotNil(rs); + + XCTAssertTrue([rs next]); + + XCTAssertEqualObjects([rs stringForColumn:@"t4.a"], @"one"); + XCTAssertEqualObjects([rs stringForColumn:@"b"], @"two"); + + XCTAssertEqual(strcmp((const char*)[rs UTF8StringForColumnName:@"b"], "two"), 0, @"String comparison should return zero"); + + [rs close]; + + // let's try these again, with the withArgumentsInArray: variation + XCTAssertTrue([self.db executeUpdate:@"drop table t4;" withArgumentsInArray:[NSArray array]]); + XCTAssertTrue([self.db executeUpdate:@"create table t4 (a text, b text)" withArgumentsInArray:[NSArray array]]); + + [self.db executeUpdate:@"insert into t4 (a, b) values (?, ?)" withArgumentsInArray:[NSArray arrayWithObjects:@"one", @"two", nil]]; + + rs = [self.db executeQuery:@"select t4.a as 't4.a', t4.b from t4;" withArgumentsInArray:[NSArray array]]; + + XCTAssertNotNil(rs); + + XCTAssertTrue([rs next]); + + XCTAssertEqualObjects([rs stringForColumn:@"t4.a"], @"one"); + XCTAssertEqualObjects([rs stringForColumn:@"b"], @"two"); + + XCTAssertEqual(strcmp((const char*)[rs UTF8StringForColumnName:@"b"], "two"), 0, @"String comparison should return zero"); + + [rs close]; +} + +- (void)testFormatStringParsing +{ + XCTAssertTrue([self.db executeUpdate:@"create table t5 (a text, b int, c blob, d text, e text)"]); + [self.db executeUpdateWithFormat:@"insert into t5 values (%s, %d, %@, %c, %lld)", "text", 42, @"BLOB", 'd', 12345678901234ll]; + + FMResultSet *rs = [self.db executeQueryWithFormat:@"select * from t5 where a = %s and a = %@ and b = %d", "text", @"text", 42]; + XCTAssertNotNil(rs); + + XCTAssertTrue([rs next]); + + XCTAssertEqualObjects([rs stringForColumn:@"a"], @"text"); + XCTAssertEqual([rs intForColumn:@"b"], 42); + XCTAssertEqualObjects([rs stringForColumn:@"c"], @"BLOB"); + XCTAssertEqualObjects([rs stringForColumn:@"d"], @"d"); + XCTAssertEqual([rs longLongIntForColumn:@"e"], 12345678901234ll); + + [rs close]; +} + +- (void)testFormatStringParsingWithSizePrefixes +{ + XCTAssertTrue([self.db executeUpdate:@"create table t55 (a text, b int, c float)"]); + short testShort = -4; + float testFloat = 5.5; + [self.db executeUpdateWithFormat:@"insert into t55 values (%c, %hi, %g)", 'a', testShort, testFloat]; + + unsigned short testUShort = 6; + [self.db executeUpdateWithFormat:@"insert into t55 values (%c, %hu, %g)", 'a', testUShort, testFloat]; + + + FMResultSet *rs = [self.db executeQueryWithFormat:@"select * from t55 where a = %s order by 2", "a"]; + XCTAssertNotNil(rs); + + XCTAssertTrue([rs next]); + + XCTAssertEqualObjects([rs stringForColumn:@"a"], @"a"); + XCTAssertEqual([rs intForColumn:@"b"], -4); + XCTAssertEqualObjects([rs stringForColumn:@"c"], @"5.5"); + + + XCTAssertTrue([rs next]); + + XCTAssertEqualObjects([rs stringForColumn:@"a"], @"a"); + XCTAssertEqual([rs intForColumn:@"b"], 6); + XCTAssertEqualObjects([rs stringForColumn:@"c"], @"5.5"); + + [rs close]; +} + +- (void)testFormatStringParsingWithNilValue +{ + XCTAssertTrue([self.db executeUpdate:@"create table tatwhat (a text)"]); + + BOOL worked = [self.db executeUpdateWithFormat:@"insert into tatwhat values(%@)", nil]; + + XCTAssertTrue(worked); + + FMResultSet *rs = [self.db executeQueryWithFormat:@"select * from tatwhat"]; + XCTAssertNotNil(rs); + XCTAssertTrue([rs next]); + XCTAssertTrue([rs columnIndexIsNull:0]); + + XCTAssertFalse([rs next]); +} + +- (void)testUpdateWithErrorAndBindings +{ + XCTAssertTrue([self.db executeUpdate:@"create table t5 (a text, b int, c blob, d text, e text)"]); + + NSError *err = nil; + BOOL result = [self.db update:@"insert into t5 values (?, ?, ?, ?, ?)" withErrorAndBindings:&err, @"text", [NSNumber numberWithInt:42], @"BLOB", @"d", [NSNumber numberWithInt:0]]; + XCTAssertTrue(result); +} + +- (void)testSelectWithEmptyArgumentsArray +{ + FMResultSet *rs = [self.db executeQuery:@"select * from test where a=?" withArgumentsInArray:@[]]; + XCTAssertNil(rs); +} + +- (void)testDatabaseAttach +{ + NSFileManager *fileManager = [NSFileManager new]; + [fileManager removeItemAtPath:@"/tmp/attachme.db" error:nil]; + + FMDatabase *dbB = [FMDatabase databaseWithPath:@"/tmp/attachme.db"]; + XCTAssertTrue([dbB open]); + XCTAssertTrue([dbB executeUpdate:@"create table attached (a text)"]); + XCTAssertTrue(([dbB executeUpdate:@"insert into attached values (?)", @"test"])); + XCTAssertTrue([dbB close]); + + [self.db executeUpdate:@"attach database '/tmp/attachme.db' as attack"]; + + FMResultSet *rs = [self.db executeQuery:@"select * from attack.attached"]; + XCTAssertNotNil(rs); + XCTAssertTrue([rs next]); + [rs close]; +} + +- (void)testNamedParameters +{ + // ------------------------------------------------------------------------------- + // Named parameters. + XCTAssertTrue([self.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"]; + XCTAssertTrue([self.db executeUpdate:@"insert into namedparamtest values (:a, :b, :c, :d)" withParameterDictionary:dictionaryArgs]); + + FMResultSet *rs = [self.db executeQuery:@"select * from namedparamtest"]; + + XCTAssertNotNil(rs); + XCTAssertTrue([rs next]); + + XCTAssertEqualObjects([rs stringForColumn:@"a"], @"Text1"); + XCTAssertEqualObjects([rs stringForColumn:@"b"], @"Text2"); + XCTAssertEqual([rs intForColumn:@"c"], 1); + XCTAssertEqual([rs doubleForColumn:@"d"], 2.0); + + [rs close]; + + + dictionaryArgs = [NSMutableDictionary dictionary]; + + [dictionaryArgs setObject:@"Text2" forKey:@"blah"]; + + rs = [self.db executeQuery:@"select * from namedparamtest where b = :blah" withParameterDictionary:dictionaryArgs]; + + XCTAssertNotNil(rs); + XCTAssertTrue([rs next]); + + XCTAssertEqualObjects([rs stringForColumn:@"b"], @"Text2"); + + [rs close]; +} + +- (void)testPragmaDatabaseList +{ + FMResultSet *rs = [self.db executeQuery:@"PRAGMA database_list"]; + int counter = 0; + while ([rs next]) { + counter++; + XCTAssertEqualObjects([rs stringForColumn:@"file"], self.databasePath); + } + XCTAssertEqual(counter, 1, @"Only one database should be attached"); +} + +- (void)testCachedStatementsInUse +{ + [self.db setShouldCacheStatements:true]; + + [self.db executeUpdate:@"CREATE TABLE testCacheStatements(key INTEGER PRIMARY KEY, value INTEGER)"]; + [self.db executeUpdate:@"INSERT INTO testCacheStatements (key, value) VALUES (1, 2)"]; + [self.db executeUpdate:@"INSERT INTO testCacheStatements (key, value) VALUES (2, 4)"]; + + XCTAssertTrue([[self.db executeQuery:@"SELECT * FROM testCacheStatements WHERE key=1"] next]); + XCTAssertTrue([[self.db executeQuery:@"SELECT * FROM testCacheStatements WHERE key=1"] next]); +} + +- (void)testStatementCachingWorks +{ + [self.db executeUpdate:@"CREATE TABLE testStatementCaching ( value INTEGER )"]; + [self.db executeUpdate:@"INSERT INTO testStatementCaching( value ) VALUES (1)"]; + [self.db executeUpdate:@"INSERT INTO testStatementCaching( value ) VALUES (1)"]; + [self.db executeUpdate:@"INSERT INTO testStatementCaching( value ) VALUES (2)"]; + + [self.db setShouldCacheStatements:YES]; + + // two iterations. + // the first time through no statements will be from the cache. + // the second time through all statements come from the cache. + for (int i = 1; i <= 2; i++ ) { + + FMResultSet* rs1 = [self.db executeQuery: @"SELECT rowid, * FROM testStatementCaching WHERE value = ?", @1]; // results in 2 rows... + XCTAssertNotNil(rs1); + XCTAssertTrue([rs1 next]); + + // confirm that we're seeing the benefits of caching. + XCTAssertEqual([[rs1 statement] useCount], (long)i); + + FMResultSet* rs2 = [self.db executeQuery:@"SELECT rowid, * FROM testStatementCaching WHERE value = ?", @2]; // results in 1 row + XCTAssertNotNil(rs2); + XCTAssertTrue([rs2 next]); + XCTAssertEqual([[rs2 statement] useCount], (long)i); + + // This is the primary check - with the old implementation of statement caching, rs2 would have rejiggered the (cached) statement used by rs1, making this test fail to return the 2nd row in rs1. + XCTAssertTrue([rs1 next]); + + [rs1 close]; + [rs2 close]; + } + +} + +/* + Test the date format + */ + +- (void)testDateFormat +{ + void (^testOneDateFormat)(FMDatabase *, NSDate *) = ^( 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"]; + XCTAssertNotNil(rs); + XCTAssertTrue([rs next]); + + XCTAssertEqualObjects([rs dateForColumnIndex:0], testDate); + + [rs close]; + }; + + 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(self.db,testDate); + + // now test the string-based timestamp + [self.db setDateFormat:fmt]; + testOneDateFormat(self.db, testDate); +} + +- (void)testColumnNameMap +{ + XCTAssertTrue([self.db executeUpdate:@"create table colNameTest (a, b, c, d)"]); + XCTAssertTrue([self.db executeUpdate:@"insert into colNameTest values (1, 2, 3, 4)"]); + + FMResultSet *ars = [self.db executeQuery:@"select * from colNameTest"]; + XCTAssertNotNil(ars); + + NSDictionary *d = [ars columnNameToIndexMap]; + XCTAssertEqual([d count], (NSUInteger)4); + + XCTAssertEqualObjects([d objectForKey:@"a"], @0); + XCTAssertEqualObjects([d objectForKey:@"b"], @1); + XCTAssertEqualObjects([d objectForKey:@"c"], @2); + XCTAssertEqualObjects([d objectForKey:@"d"], @3); + +} + +- (void)testCustomFunction +{ + [self.db executeUpdate:@"create table ftest (foo text)"]; + [self.db executeUpdate:@"insert into ftest values ('hello')"]; + [self.db executeUpdate:@"insert into ftest values ('hi')"]; + [self.db executeUpdate:@"insert into ftest values ('not h!')"]; + [self.db executeUpdate:@"insert into ftest values ('definitely not h!')"]; + + [self.db 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 { + XCTFail(@"Unknown format for StringStartsWithH (%d)", sqlite3_value_type(aargv[0])); + sqlite3_result_null(context); + } + }]; + + int rowCount = 0; + FMResultSet *ars = [self.db executeQuery:@"select * from ftest where StringStartsWithH(foo)"]; + while ([ars next]) { + rowCount++; + + } + XCTAssertEqual(rowCount, 2); + +} + + +#if SQLITE_VERSION_NUMBER >= 3007017 +- (void)testApplicationID +{ + uint32_t appID = NSHFSTypeCodeFromFileType(NSFileTypeForHFSTypeCode('fmdb')); + + [db setApplicationID:appID]; + + uint32_t rAppID = [db applicationID]; + + XCTAssertEqual(rAppID, appID); + + [db setApplicationIDString:@"acrn"]; + + NSString *s = [db applicationIDString]; + + XCTAssertEqualObjects(s, @"acrn"); +} +#endif + + +@end diff --git a/Tests/Schemes/Tests.xcscheme b/Tests/Schemes/Tests.xcscheme new file mode 100644 index 0000000..3ccd45a --- /dev/null +++ b/Tests/Schemes/Tests.xcscheme @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "0500" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "EE4290EE12B42F870088BD94" + BuildableName = "libFMDB.a" + BlueprintName = "FMDB" + ReferencedContainer = "container:fmdb.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "NO" + buildForArchiving = "NO" + buildForAnalyzing = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "BF5D041518416BB2008C5AA9" + BuildableName = "Tests.xctest" + BlueprintName = "Tests" + ReferencedContainer = "container:fmdb.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES" + buildConfiguration = "Debug"> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "BF5D041518416BB2008C5AA9" + BuildableName = "Tests.xctest" + BlueprintName = "Tests" + ReferencedContainer = "container:fmdb.xcodeproj"> + </BuildableReference> + </TestableReference> + </Testables> + </TestAction> + <LaunchAction + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + buildConfiguration = "Debug" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + allowLocationSimulation = "YES"> + <AdditionalOptions> + </AdditionalOptions> + </LaunchAction> + <ProfileAction + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + buildConfiguration = "Release" + debugDocumentVersioning = "YES"> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Tests/Tests-Info.plist b/Tests/Tests-Info.plist new file mode 100644 index 0000000..30e7ee2 --- /dev/null +++ b/Tests/Tests-Info.plist @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>me.grahamdennis.${PRODUCT_NAME:rfc1034identifier}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>BNDL</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1</string> +</dict> +</plist> diff --git a/Tests/Tests-Prefix.pch b/Tests/Tests-Prefix.pch new file mode 100644 index 0000000..930e2c9 --- /dev/null +++ b/Tests/Tests-Prefix.pch @@ -0,0 +1,11 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#ifdef __OBJC__ + #import <Cocoa/Cocoa.h> + #import <XCTest/XCTest.h> + #import "FMDBTempDBTests.h" +#endif diff --git a/Tests/en.lproj/InfoPlist.strings b/Tests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/Tests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/fmdb.xcodeproj/project.pbxproj b/fmdb.xcodeproj/project.pbxproj index 150d841..813465c 100644 --- a/fmdb.xcodeproj/project.pbxproj +++ b/fmdb.xcodeproj/project.pbxproj @@ -9,6 +9,16 @@ /* Begin PBXBuildFile section */ 8DD76F9C0486AA7600D96B5E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08FB779EFE84155DC02AAC07 /* Foundation.framework */; }; 8DD76F9F0486AA7600D96B5E /* fmdb.1 in CopyFiles */ = {isa = PBXBuildFile; fileRef = C6859EA3029092ED04C91782 /* fmdb.1 */; }; + BF5D041918416BB2008C5AA9 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF5D041818416BB2008C5AA9 /* XCTest.framework */; }; + BF5D041F18416BB2008C5AA9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF5D041D18416BB2008C5AA9 /* InfoPlist.strings */; }; + BF5D042118416BB2008C5AA9 /* FMDatabaseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5D042018416BB2008C5AA9 /* FMDatabaseTests.m */; }; + BF5D04281841702E008C5AA9 /* libFMDB.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EE4290EF12B42F870088BD94 /* libFMDB.a */; }; + BF5D042918417159008C5AA9 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = EE42910C12B42FFA0088BD94 /* libsqlite3.dylib */; }; + BF940F5C18417D490001E077 /* FMDBTempDBTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BF940F5B18417D490001E077 /* FMDBTempDBTests.m */; }; + BF940F5E18417DEA0001E077 /* FMDatabaseAdditionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BF940F5D18417DEA0001E077 /* FMDatabaseAdditionsTests.m */; }; + BFC152B118417F0D00605DF7 /* FMDatabaseAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CC50F2CB0DF9183600E4AAAE /* FMDatabaseAdditions.m */; }; + BFE55E131841C9A000CB3A63 /* FMDatabasePoolTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BFE55E121841C9A000CB3A63 /* FMDatabasePoolTests.m */; }; + BFE55E151841D38800CB3A63 /* FMDatabaseQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BFE55E141841D38800CB3A63 /* FMDatabaseQueueTests.m */; }; CC47A00F148581E9002CCDAB /* FMDatabaseQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = CC47A00D148581E9002CCDAB /* FMDatabaseQueue.h */; }; CC47A010148581E9002CCDAB /* FMDatabaseQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = CC47A00E148581E9002CCDAB /* FMDatabaseQueue.m */; }; CC47A011148581E9002CCDAB /* FMDatabaseQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = CC47A00E148581E9002CCDAB /* FMDatabaseQueue.m */; }; @@ -32,6 +42,16 @@ EE42910D12B42FFA0088BD94 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = EE42910C12B42FFA0088BD94 /* libsqlite3.dylib */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + BF5D042318416BB2008C5AA9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 08FB7793FE84155DC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EE4290EE12B42F870088BD94; + remoteInfo = FMDB; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 8DD76F9E0486AA7600D96B5E /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; @@ -52,6 +72,17 @@ 32A70AAB03705E1F00C91783 /* fmdb_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fmdb_Prefix.pch; sourceTree = "<group>"; }; 831DE6FD175B7C9C001F7317 /* README.markdown */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.markdown; sourceTree = "<group>"; }; 8DD76FA10486AA7600D96B5E /* fmdb */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = fmdb; sourceTree = BUILT_PRODUCTS_DIR; }; + BF5D041618416BB2008C5AA9 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BF5D041818416BB2008C5AA9 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + BF5D041C18416BB2008C5AA9 /* Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = "<group>"; }; + BF5D041E18416BB2008C5AA9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + BF5D042018416BB2008C5AA9 /* FMDatabaseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FMDatabaseTests.m; sourceTree = "<group>"; }; + BF5D042218416BB2008C5AA9 /* Tests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tests-Prefix.pch"; sourceTree = "<group>"; }; + BF940F5A18417D490001E077 /* FMDBTempDBTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDBTempDBTests.h; sourceTree = "<group>"; }; + BF940F5B18417D490001E077 /* FMDBTempDBTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDBTempDBTests.m; sourceTree = "<group>"; }; + BF940F5D18417DEA0001E077 /* FMDatabaseAdditionsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabaseAdditionsTests.m; sourceTree = "<group>"; }; + BFE55E121841C9A000CB3A63 /* FMDatabasePoolTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabasePoolTests.m; sourceTree = "<group>"; }; + BFE55E141841D38800CB3A63 /* FMDatabaseQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabaseQueueTests.m; sourceTree = "<group>"; }; C6859EA3029092ED04C91782 /* fmdb.1 */ = {isa = PBXFileReference; lastKnownFileType = text.man; path = fmdb.1; sourceTree = "<group>"; }; CC47A00D148581E9002CCDAB /* FMDatabaseQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FMDatabaseQueue.h; path = src/FMDatabaseQueue.h; sourceTree = "<group>"; }; CC47A00E148581E9002CCDAB /* FMDatabaseQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FMDatabaseQueue.m; path = src/FMDatabaseQueue.m; sourceTree = "<group>"; }; @@ -84,6 +115,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BF5D041318416BB2008C5AA9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BF5D042918417159008C5AA9 /* libsqlite3.dylib in Frameworks */, + BF5D04281841702E008C5AA9 /* libFMDB.a in Frameworks */, + BF5D041918416BB2008C5AA9 /* XCTest.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; EE4290ED12B42F870088BD94 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -105,6 +146,8 @@ 08FB7795FE84155DC02AAC07 /* Source */, C6859EA2029092E104C91782 /* Documentation */, 08FB779DFE84155DC02AAC07 /* External Frameworks and Libraries */, + BF5D041A18416BB2008C5AA9 /* Tests */, + BF5D041718416BB2008C5AA9 /* Frameworks */, 1AB674ADFE9D54B511CA2CBB /* Products */, EE42910C12B42FFA0088BD94 /* libsqlite3.dylib */, ); @@ -145,10 +188,43 @@ children = ( 8DD76FA10486AA7600D96B5E /* fmdb */, EE4290EF12B42F870088BD94 /* libFMDB.a */, + BF5D041618416BB2008C5AA9 /* Tests.xctest */, ); name = Products; sourceTree = "<group>"; }; + BF5D041718416BB2008C5AA9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + BF5D041818416BB2008C5AA9 /* XCTest.framework */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; + BF5D041A18416BB2008C5AA9 /* Tests */ = { + isa = PBXGroup; + children = ( + BF940F5A18417D490001E077 /* FMDBTempDBTests.h */, + BF940F5B18417D490001E077 /* FMDBTempDBTests.m */, + BF5D042018416BB2008C5AA9 /* FMDatabaseTests.m */, + BFE55E121841C9A000CB3A63 /* FMDatabasePoolTests.m */, + BFE55E141841D38800CB3A63 /* FMDatabaseQueueTests.m */, + BF940F5D18417DEA0001E077 /* FMDatabaseAdditionsTests.m */, + BF5D041B18416BB2008C5AA9 /* Supporting Files */, + ); + path = Tests; + sourceTree = "<group>"; + }; + BF5D041B18416BB2008C5AA9 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + BF5D041C18416BB2008C5AA9 /* Tests-Info.plist */, + BF5D041D18416BB2008C5AA9 /* InfoPlist.strings */, + BF5D042218416BB2008C5AA9 /* Tests-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = "<group>"; + }; C6859EA2029092E104C91782 /* Documentation */ = { isa = PBXGroup; children = ( @@ -193,6 +269,24 @@ productReference = 8DD76FA10486AA7600D96B5E /* fmdb */; productType = "com.apple.product-type.tool"; }; + BF5D041518416BB2008C5AA9 /* Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BF5D042718416BB2008C5AA9 /* Build configuration list for PBXNativeTarget "Tests" */; + buildPhases = ( + BF5D041218416BB2008C5AA9 /* Sources */, + BF5D041318416BB2008C5AA9 /* Frameworks */, + BF5D041418416BB2008C5AA9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BF5D042418416BB2008C5AA9 /* PBXTargetDependency */, + ); + name = Tests; + productName = Tests; + productReference = BF5D041618416BB2008C5AA9 /* Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; EE4290EE12B42F870088BD94 /* FMDB */ = { isa = PBXNativeTarget; buildConfigurationList = EE42910012B42FA00088BD94 /* Build configuration list for PBXNativeTarget "FMDB" */; @@ -217,6 +311,11 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 0460; + TargetAttributes = { + BF5D041518416BB2008C5AA9 = { + TestTargetID = EE4290EE12B42F870088BD94; + }; + }; }; buildConfigurationList = 1DEB927808733DD40010E9CD /* Build configuration list for PBXProject "fmdb" */; compatibilityVersion = "Xcode 3.2"; @@ -227,6 +326,7 @@ Japanese, French, German, + en, ); mainGroup = 08FB7794FE84155DC02AAC07 /* fmdb */; projectDirPath = ""; @@ -234,10 +334,22 @@ targets = ( 8DD76F960486AA7600D96B5E /* fmdb */, EE4290EE12B42F870088BD94 /* FMDB */, + BF5D041518416BB2008C5AA9 /* Tests */, ); }; /* End PBXProject section */ +/* Begin PBXResourcesBuildPhase section */ + BF5D041418416BB2008C5AA9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BF5D041F18416BB2008C5AA9 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 8DD76F990486AA7600D96B5E /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -252,6 +364,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BF5D041218416BB2008C5AA9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BFC152B118417F0D00605DF7 /* FMDatabaseAdditions.m in Sources */, + BF940F5C18417D490001E077 /* FMDBTempDBTests.m in Sources */, + BF940F5E18417DEA0001E077 /* FMDatabaseAdditionsTests.m in Sources */, + BF5D042118416BB2008C5AA9 /* FMDatabaseTests.m in Sources */, + BFE55E131841C9A000CB3A63 /* FMDatabasePoolTests.m in Sources */, + BFE55E151841D38800CB3A63 /* FMDatabaseQueueTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; EE4290EC12B42F870088BD94 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -266,6 +391,25 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + BF5D042418416BB2008C5AA9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EE4290EE12B42F870088BD94 /* FMDB */; + targetProxy = BF5D042318416BB2008C5AA9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + BF5D041D18416BB2008C5AA9 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + BF5D041E18416BB2008C5AA9 /* en */, + ); + name = InfoPlist.strings; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 1DEB927508733DD40010E9CD /* Debug */ = { isa = XCBuildConfiguration; @@ -341,6 +485,80 @@ }; name = Release; }; + BF5D042518416BB2008C5AA9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Tests/Tests-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_PEDANTIC = NO; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + INFOPLIST_FILE = "Tests/Tests-Info.plist"; + MACOSX_DEPLOYMENT_TARGET = 10.8; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + BF5D042618416BB2008C5AA9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Tests/Tests-Prefix.pch"; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + INFOPLIST_FILE = "Tests/Tests-Info.plist"; + MACOSX_DEPLOYMENT_TARGET = 10.8; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; EE4290F012B42F880088BD94 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -386,6 +604,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + BF5D042718416BB2008C5AA9 /* Build configuration list for PBXNativeTarget "Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BF5D042518416BB2008C5AA9 /* Debug */, + BF5D042618416BB2008C5AA9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; EE42910012B42FA00088BD94 /* Build configuration list for PBXNativeTarget "FMDB" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/src/FMDatabase.h b/src/FMDatabase.h index 8169173..c538f58 100644 --- a/src/FMDatabase.h +++ b/src/FMDatabase.h @@ -24,25 +24,14 @@ #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 +// If OS_OBJECT_USE_OBJC=1, then the dispatch objects will be treated like ObjC objects +// and will participate in ARC. +// See the section on "Dispatch Queues and Automatic Reference Counting" in "Grand Central Dispatch (GCD) Reference" for details. + #if OS_OBJECT_USE_OBJC + #define FMDBDispatchQueueRelease(__v) + #else + #define FMDBDispatchQueueRelease(__v) (dispatch_release(__v)); + #endif #endif #if !__has_feature(objc_instancetype) diff --git a/src/FMDatabasePool.h b/src/FMDatabasePool.h index 5af0305..3e1d53b 100644 --- a/src/FMDatabasePool.h +++ b/src/FMDatabasePool.h @@ -40,11 +40,13 @@ __unsafe_unretained id _delegate; NSUInteger _maximumNumberOfDatabasesToCreate; + int _openFlags; } @property (atomic, retain) NSString *path; @property (atomic, assign) id delegate; @property (atomic, assign) NSUInteger maximumNumberOfDatabasesToCreate; +@property (atomic, readonly) int openFlags; ///--------------------- /// @name Initialization @@ -59,6 +61,16 @@ + (instancetype)databasePoolWithPath:(NSString*)aPath; +/** Create pool using path and specified flags + + @param aPath The file path of the database. + @param openFlags Flags passed to the openWithFlags method of the database + + @return The `FMDatabasePool` object. `nil` on error. + */ + ++ (instancetype)databasePoolWithPath:(NSString*)aPath flags:(int)openFlags; + /** Create pool using path. @param aPath The file path of the database. @@ -68,6 +80,16 @@ - (instancetype)initWithPath:(NSString*)aPath; +/** Create pool using path and specified flags. + + @param aPath The file path of the database. + @param openFlags Flags passed to the openWithFlags method of the database + + @return The `FMDatabasePool` object. `nil` on error. + */ + +- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags; + ///------------------------------------------------ /// @name Keeping track of checked in/out databases ///------------------------------------------------ diff --git a/src/FMDatabasePool.m b/src/FMDatabasePool.m index ee0611b..ccf4628 100644 --- a/src/FMDatabasePool.m +++ b/src/FMDatabasePool.m @@ -21,17 +21,18 @@ @synthesize path=_path; @synthesize delegate=_delegate; @synthesize maximumNumberOfDatabasesToCreate=_maximumNumberOfDatabasesToCreate; +@synthesize openFlags=_openFlags; + (instancetype)databasePoolWithPath:(NSString*)aPath { return FMDBReturnAutoreleased([[self alloc] initWithPath:aPath]); } -- (instancetype)init { - return [self initWithPath:nil]; ++ (instancetype)databasePoolWithPath:(NSString*)aPath flags:(int)openFlags { + return FMDBReturnAutoreleased([[self alloc] initWithPath:aPath flags:openFlags]); } -- (instancetype)initWithPath:(NSString*)aPath { +- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags { self = [super init]; @@ -40,11 +41,23 @@ _lockQueue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL); _databaseInPool = FMDBReturnRetained([NSMutableArray array]); _databaseOutPool = FMDBReturnRetained([NSMutableArray array]); + _openFlags = openFlags; } return self; } +- (instancetype)initWithPath:(NSString*)aPath +{ + // default flags for sqlite3_open + return [self initWithPath:aPath flags:SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE]; +} + +- (instancetype)init { + return [self initWithPath:nil]; +} + + - (void)dealloc { _delegate = 0x00; @@ -110,7 +123,11 @@ } //This ensures that the db is opened before returning +#if SQLITE_VERSION_NUMBER >= 3005000 + if ([db openWithFlags:_openFlags]) { +#else if ([db open]) { +#endif if ([_delegate respondsToSelector:@selector(databasePool:shouldAddDatabaseToPool:)] && ![_delegate databasePool:self shouldAddDatabaseToPool:db]) { [db close]; db = 0x00; diff --git a/src/FMDatabaseQueue.h b/src/FMDatabaseQueue.h index 555c2d2..daf6520 100644 --- a/src/FMDatabaseQueue.h +++ b/src/FMDatabaseQueue.h @@ -69,7 +69,7 @@ } @property (atomic, retain) NSString *path; -@property (atomic) int openFlags; +@property (atomic, readonly) int openFlags; ///---------------------------------------------------- /// @name Initialization, opening, and closing of queue @@ -112,6 +112,15 @@ - (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags; +/** Returns the Class of 'FMDatabase' subclass, that will be used to instantiate database object. + + Subclasses can override this method to return specified Class of 'FMDatabase' subclass. + + @return The Class of 'FMDatabase' subclass, that will be used to instantiate database object. + */ + ++ (Class)databaseClass; + /** Close database used by queue. */ - (void)close; diff --git a/src/FMDatabaseQueue.m b/src/FMDatabaseQueue.m index 797a366..811a6d1 100644 --- a/src/FMDatabaseQueue.m +++ b/src/FMDatabaseQueue.m @@ -40,6 +40,9 @@ return q; } ++ (Class)databaseClass { + return [FMDatabase class]; +} - (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags { @@ -47,7 +50,7 @@ if (self != nil) { - _db = [FMDatabase databaseWithPath:aPath]; + _db = [[[self class] databaseClass] databaseWithPath:aPath]; FMDBRetain(_db); #if SQLITE_VERSION_NUMBER >= 3005000 @@ -111,7 +114,7 @@ #if SQLITE_VERSION_NUMBER >= 3005000 if (![_db openWithFlags:_openFlags]) { #else - if (![db open]) + if (![db open]) { #endif NSLog(@"FMDatabaseQueue could not reopen database for path %@", _path); FMDBRelease(_db); @@ -1377,6 +1377,20 @@ void testPool(NSString *dbPath) { NSLog(@"Number of open databases after crazy gcd stuff: %ld", [dbPool countOfOpenDatabases]); } + FMDatabasePool *dbPool2 = [FMDatabasePool databasePoolWithPath:dbPath flags:SQLITE_OPEN_READONLY]; + + FMDBQuickCheck(dbPool2); + { + [dbPool2 inDatabase:^(FMDatabase *db2) { + FMResultSet *rs1 = [db2 executeQuery:@"SELECT * FROM test"]; + FMDBQuickCheck(rs1 != nil); + [rs1 close]; + + BOOL ok = [db2 executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:3]]; + FMDBQuickCheck(!ok); + }]; + } + // 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 |