Skip to content

Commit

Permalink
Merge pull request #584 from robertmryan/master
Browse files Browse the repository at this point in the history
2.7 - Audit for nullability
  • Loading branch information
robertmryan committed May 26, 2017
2 parents 08d7400 + 9756de3 commit c1653c0
Show file tree
Hide file tree
Showing 25 changed files with 1,224 additions and 589 deletions.
2 changes: 1 addition & 1 deletion FMDB.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'FMDB'
s.version = '2.6.2'
s.version = '2.7'
s.summary = 'A Cocoa / Objective-C wrapper around SQLite.'
s.homepage = 'https://github.com/ccgus/fmdb'
s.license = 'MIT'
Expand Down
121 changes: 107 additions & 14 deletions README.markdown
@@ -1,4 +1,4 @@
# FMDB v2.6.2
# FMDB v2.7

This is an Objective-C wrapper around SQLite: http://sqlite.org/

Expand Down Expand Up @@ -36,6 +36,102 @@ http://ccgus.github.io/fmdb/html/index.html
## Automatic Reference Counting (ARC) or Manual Memory Management?
You can use either style in your Cocoa project. FMDB will figure out which you are using at compile time and do the right thing.

## What's New in FMDB 2.7

FMDB 2.7 attempts to support a more natural interface. This represents a fairly significant change for Swift developers (audited for nullability; shifted to properties in external interfaces where possible rather than methods; etc.). For Objective-C developers, this should be a fairly seamless transition (unless you were using the ivars that were previously exposed in the public interface, which you shouldn't have been doing, anyway!).

### Nullability and Swift Optionals

FMDB 2.7 is largely the same as prior versions, but has been audited for nullability. For Objective-C users, this simply means that if you perform a static analysis of your FMDB-based project, you may receive more meaningful warnings as you review your project, but there are likely to be few, if any, changes necessary in your code.

For Swift users, this nullability audit results in changes that are not entirely backward compatible with FMDB 2.6, but is a little more Swifty. Before FMDB was audited for nullability, Swift was forced to defensively assume that variables were optional, but the library now more accurately knows which properties and method parameters are optional, and which are not.

This means, though, that Swift code written for FMDB 2.7 may require changes. For example, consider the following Swift 3 code written for FMDB 2.6:
```swift

guard let queue = FMDatabaseQueue(path: fileURL.path) else {
print("Unable to create FMDatabaseQueue")
return
}

queue.inTransaction { db, rollback in
do {
guard let db == db else {
// handle error here
return
}

try db.executeUpdate("INSERT INTO foo (bar) VALUES (?)", values: [1])
try db.executeUpdate("INSERT INTO foo (bar) VALUES (?)", values: [2])
} catch {
rollback?.pointee = true
}
}
```

Because FMDB 2.6 was not audited for nullability, Swift inferred that `db` and `rollback` were optionals. But, now, in FMDB 2.7, Swift now knows that, for example, neither `db` nor `rollback` above can be `nil`, so they are no longer optionals. Thus it becomes:

```swift

let queue = FMDatabaseQueue(url: fileURL)

queue.inTransaction { db, rollback in
do {
try db.executeUpdate("INSERT INTO foo (bar) VALUES (?)", values: [1])
try db.executeUpdate("INSERT INTO foo (bar) VALUES (?)", values: [2])
} catch {
rollback.pointee = true
}
}
```

### Custom Functions

In the past, when writing custom functions, you would have to generally include your own `@autoreleasepool` block to avoid problems when writing functions that scanned through a large table. Now, FMDB will automatically wrap it in an autorelease pool, so you don't have to.

Also, in the past, when retrieving the values passed to the function, you had to drop down to the SQLite C API and include your own `sqlite3_value_XXX` calls. There are now `FMDatabase` methods, `valueInt`, `valueString`, etc., so you can stay within Swift and/or Objective-C, without needing to call the C functions yourself. Likewise, when specifying the return values, you no longer need to call `sqlite3_result_XXX` C API, but rather you can use `FMDatabase` methods, `resultInt`, `resultString`, etc. There is a new `enum` for `valueType` called `SqliteValueType`, which can be used for checking the type of parameter passed to the custom function.

Thus, you can do something like (in Swift 3):

```swift
db.makeFunctionNamed("RemoveDiacritics", arguments: 1) { context, argc, argv in
guard db.valueType(argv[0]) == .text || db.valueType(argv[0]) == .null else {
db.resultError("Expected string parameter", context: context)
return
}

if let string = db.valueString(argv[0])?.folding(options: .diacriticInsensitive, locale: nil) {
db.resultString(string, context: context)
} else {
db.resultNull(context: context)
}
}
```

And you can then use that function in your SQL (in this case, matching both "Jose" and "José"):

```sql
SELECT * FROM employees WHERE RemoveDiacritics(first_name) LIKE 'jose'
```

Note, the method `makeFunctionNamed:maximumArguments:withBlock:` has been renamed to `makeFunctionNamed:arguments:block:`, to more accurately reflect the functional intent of the second parameter.

### API Changes

In addition to the `makeFunctionNamed` noted above, there are a few other API changes. Specifically,

- To become consistent with the rest of the API, the methods `objectForColumnName` and `UTF8StringForColumnName` have been renamed to `objectForColumn` and `UTF8StringForColumn`.

- Note, the `objectForColumn` (and the associted subscript operator) now returns `nil` if an invalid column name/index is passed to it. It used to return `NSNull`.

- To avoid confusion with `FMDatabaseQueue` method `inTransaction`, which performs transactions, the `FMDatabase` method to determine whether you are in a transaction or not, `inTransaction`, has been replaced with a read-only property, `isInTransaction`.

- Several functions have been converted to properties, namely, `databasePath`, `maxBusyRetryTimeInterval`, `shouldCacheStatements`, `sqliteHandle`, `hasOpenResultSets`, `lastInsertRowId`, `changes`, `goodConnection`, `columnCount`, `resultDictionary`, `applicationID`, `applicationIDString`, `userVersion`, `countOfCheckedInDatabases`, `countOfCheckedOutDatabases`, and `countOfOpenDatabases`. For Objective-C users, this has little material impact, but for Swift users, it results in a slightly more natural interface. Note: For Objective-C developers, previously versions of FMDB exposed many ivars (but we hope you weren't using them directly, anyway!), but the implmentation details for these are no longer exposed.

### URL Methods

In keeping with Apple's shift from paths to URLs, there are now `NSURL` renditions of the various `init` methods, previously only accepting paths.

## Usage
There are three main classes in FMDB:

Expand Down Expand Up @@ -110,8 +206,8 @@ if ([s next]) {
- `dateForColumn:`
- `dataForColumn:`
- `dataNoCopyForColumn:`
- `UTF8StringForColumnName:`
- `objectForColumnName:`
- `UTF8StringForColumn:`
- `objectForColumn:`

Each of these methods also has a `{type}ForColumnIndex:` variant that is used to retrieve the data based on the position of the column in the results, as opposed to the column's name.

Expand Down Expand Up @@ -188,7 +284,7 @@ In Swift, you would use `executeUpdate(values:)`, which not only is a concise Sw
do {
let identifier = 42
let name = "Liam O'Flaherty (\"the famous Irish author\")"
let date = NSDate()
let date = Date()
let comment: String? = nil
try db.executeUpdate("INSERT INTO authors (identifier, name, date, comment) VALUES (?, ?, ?, ?)", values: [identifier, name, date, comment ?? NSNull()])
Expand Down Expand Up @@ -269,18 +365,18 @@ The Swift 3 equivalent would be:
```swift
queue.inTransaction { db, rollback in
do {
try db?.executeUpdate("INSERT INTO myTable VALUES (?)", values: [1])
try db?.executeUpdate("INSERT INTO myTable VALUES (?)", values: [2])
try db?.executeUpdate("INSERT INTO myTable VALUES (?)", values: [3])
try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [1])
try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [2])
try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [3])
if whoopsSomethingWrongHappened {
rollback?.pointee = true
rollback.pointee = true
return
}
// etc ...
} catch {
rollback?.pointee = true
rollback.pointee = true
print(error)
}
}
Expand Down Expand Up @@ -326,10 +422,7 @@ let fileURL = try! FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("test.sqlite")

guard let database = FMDatabase(path: fileURL.path) else {
print("unable to create database")
return
}
let database = FMDatabase(url: fileURL)

guard database.open() else {
print("Unable to open database")
Expand Down Expand Up @@ -361,7 +454,7 @@ let fileURL = try! NSFileManager.defaultManager()
.URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: false)
.URLByAppendingPathComponent("test.sqlite")

let database = FMDatabase(path: fileURL.path)
let database = FMDatabase(url: fileURL)

if !database.open() {
print("Unable to open database")
Expand Down
19 changes: 14 additions & 5 deletions Tests/FMDatabaseAdditionsTests.m
Expand Up @@ -77,8 +77,19 @@ - (void)testDateForQuery
XCTAssertEqualWithAccuracy([foo timeIntervalSinceDate:date], 0.0, 1.0, @"Dates should be the same to within a second");
}

- (void)testTableExists
{
- (void)testValidate {
NSError *error;
XCTAssert([self.db validateSQL:@"create table datetest (a double, b double, c double)" error:&error]);
XCTAssertNil(error, @"There should be no error object");
}

- (void)testFailValidate {
NSError *error;
XCTAssertFalse([self.db validateSQL:@"blah blah blah" error:&error]);
XCTAssert(error, @"There should be no error object");
}

- (void)testTableExists {
XCTAssertTrue([self.db executeUpdate:@"create table t4 (a text, b text)"]);

XCTAssertTrue([self.db tableExists:@"t4"]);
Expand All @@ -91,8 +102,7 @@ - (void)testTableExists

}

- (void)testColumnExists
{
- (void)testColumnExists {
[self.db executeUpdate:@"create table nulltest (a text, b text)"];

XCTAssertTrue([self.db columnExists:@"a" inTableWithName:@"nulltest"]);
Expand All @@ -101,7 +111,6 @@ - (void)testColumnExists
}

- (void)testUserVersion {

[[self db] setUserVersion:12];

XCTAssertTrue([[self db] userVersion] == 12);
Expand Down
99 changes: 91 additions & 8 deletions Tests/FMDatabasePoolTests.m
Expand Up @@ -8,6 +8,12 @@

#import <XCTest/XCTest.h>

#if FMDB_SQLITE_STANDALONE
#import <sqlite3/sqlite3.h>
#else
#import <sqlite3.h>
#endif

@interface FMDatabasePoolTests : FMDBTempDBTests

@property FMDatabasePool *pool;
Expand All @@ -31,8 +37,7 @@ + (void)populateDatabase:(FMDatabase *)db
[db executeUpdate:@"insert into likefoo values ('not')"];
}

- (void)setUp
{
- (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.

Expand All @@ -42,19 +47,97 @@ - (void)setUp

}

- (void)tearDown
{
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}

- (void)testPoolIsInitiallyEmpty
{
- (void)testURLOpenNoURL {
FMDatabasePool *pool = [[FMDatabasePool alloc] initWithURL:nil];
XCTAssert(pool, @"Database pool should be returned");
pool = nil;
}

- (void)testURLOpen {
NSURL *tempFolder = [NSURL fileURLWithPath:NSTemporaryDirectory()];
NSURL *fileURL = [tempFolder URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]];

FMDatabasePool *pool = [FMDatabasePool databasePoolWithURL:fileURL];
XCTAssert(pool, @"Database pool should be returned");
pool = nil;
[[NSFileManager defaultManager] removeItemAtURL:fileURL error:nil];
}

- (void)testURLOpenInit {
NSURL *tempFolder = [NSURL fileURLWithPath:NSTemporaryDirectory()];
NSURL *fileURL = [tempFolder URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]];

FMDatabasePool *pool = [[FMDatabasePool alloc] initWithURL:fileURL];
XCTAssert(pool, @"Database pool should be returned");
pool = nil;
[[NSFileManager defaultManager] removeItemAtURL:fileURL error:nil];
}

- (void)testURLOpenWithOptions {
NSURL *tempFolder = [NSURL fileURLWithPath:NSTemporaryDirectory()];
NSURL *fileURL = [tempFolder URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]];

FMDatabasePool *pool = [FMDatabasePool databasePoolWithURL:fileURL flags:SQLITE_OPEN_READWRITE];
[pool inDatabase:^(FMDatabase * _Nonnull db) {
XCTAssertNil(db, @"The database should not have been created");
}];
}

- (void)testURLOpenInitWithOptions {
NSURL *tempFolder = [NSURL fileURLWithPath:NSTemporaryDirectory()];
NSURL *fileURL = [tempFolder URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]];

FMDatabasePool *pool = [[FMDatabasePool alloc] initWithURL:fileURL flags:SQLITE_OPEN_READWRITE];
[pool inDatabase:^(FMDatabase * _Nonnull db) {
XCTAssertNil(db, @"The database should not have been created");
}];

pool = [[FMDatabasePool alloc] initWithURL:fileURL flags:SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE];
[pool inDatabase:^(FMDatabase * _Nonnull db) {
XCTAssert(db, @"The database should have been created");

BOOL success = [db executeUpdate:@"CREATE TABLE foo (bar INT)"];
XCTAssert(success, @"Create failed");
success = [db executeUpdate:@"INSERT INTO foo (bar) VALUES (?)", @42];
XCTAssert(success, @"Insert failed");
}];

pool = [[FMDatabasePool alloc] initWithURL:fileURL flags:SQLITE_OPEN_READONLY];
[pool inDatabase:^(FMDatabase * _Nonnull db) {
XCTAssert(db, @"Now database pool should open have been created");
BOOL success = [db executeUpdate:@"CREATE TABLE baz (qux INT)"];
XCTAssertFalse(success, @"But updates should fail on read only database");
}];
pool = nil;

[[NSFileManager defaultManager] removeItemAtURL:fileURL error:nil];
}

- (void)testURLOpenWithOptionsVfs {
sqlite3_vfs vfs = *sqlite3_vfs_find(NULL);
vfs.zName = "MyCustomVFS";
XCTAssertEqual(SQLITE_OK, sqlite3_vfs_register(&vfs, 0));

NSURL *tempFolder = [NSURL fileURLWithPath:NSTemporaryDirectory()];
NSURL *fileURL = [tempFolder URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]];

FMDatabasePool *pool = [[FMDatabasePool alloc] initWithURL:fileURL flags:SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE vfs:@"MyCustomVFS"];
XCTAssert(pool, @"Database pool should not have been created");
pool = nil;

XCTAssertEqual(SQLITE_OK, sqlite3_vfs_unregister(&vfs));
}

- (void)testPoolIsInitiallyEmpty {
XCTAssertEqual([self.pool countOfOpenDatabases], (NSUInteger)0, @"Pool should be empty on creation");
}

- (void)testDatabaseCreation
{
- (void)testDatabaseCreation {
__block FMDatabase *db1;

[self.pool inDatabase:^(FMDatabase *db) {
Expand Down

0 comments on commit c1653c0

Please sign in to comment.