Skip to content

Commit

Permalink
Merge pull request ccgus#11 from davedelong/master.
Browse files Browse the repository at this point in the history
FMDB: now with more documentation!
  • Loading branch information
ccgus committed Apr 30, 2011
2 parents b4bcbd9 + 8d38b7e commit c2a13ff
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 6 deletions.
6 changes: 6 additions & 0 deletions CHANGES_AND_TODO_LIST.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ Zip, nada, zilch. Got any ideas?

If you would like to contribute some code- awesome! I just ask that you make it conform to the coding conventions already set in here, and to add a couple of tests for your new code to fmdb.m. And of course, the code should be of general use to more than just a couple of folks. Send your patches to gus@flyingmeat.com.

2011.04.09
Added a method to validate a SQL statement
Added a method to retrieve the number of columns in a result set
Renamed "objectForColumnName:" to "objectForColumn:"
Added two methods to execute queries and updates with NSString-style format specifiers

2011.03.12
Added compatibility with garbage collection.
When an FMDatabase is closed, all open FMResultSets pertaining to that database are also closed.
Expand Down
126 changes: 123 additions & 3 deletions README.markdown
Original file line number Diff line number Diff line change
@@ -1,4 +1,124 @@
FMDB
====
# FMDB

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

## Usage

There are two main classes in FMDB:

1. `FMDatabase` - Represents a single SQLite database. Used for executing SQL statements.
2. `FMResultSet` - Represents the results of executing a query on an `FMDatabase`.

### Database Creation
An `FMDatabase` is created with a path to a SQLite database file. This path can be one of these three:

1. A file system path. The file does not have to exist on disk. If it does not exist, it is created for you.
2. An empty string (`@""`). An empty database is created at a temporary location. This database is deleted with the `FMDatabase` connection is closed.
3. `NULL`. An in-memory database is created. This database will be destroyed with the `FMDatabase` connection is closed.

FMDatabase *db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];

### Opening

Before you can interact with the database, it must be opened. Opening fails if there are insufficient resources or permissions to open and/or create the database.

if (![db open]) {
[db release];
return;
}

### Executing Updates

Any sort of SQL statement which is not a `SELECT` statement qualifies as an update. This includes `CREATE`, `PRAGMA`, `UPDATE`, `INSERT`, `ALTER`, `COMMIT`, `BEGIN`, `DETACH`, `DELETE`, `DROP`, `END`, `EXPLAIN`, `VACUUM`, and `REPLACE` statements (plus many more). Basically, if your SQL statement does not begin with `SELECT`, it is an update statement.

Executing updates returns a single value, a `BOOL`. A return value of `YES` means the update was successfully executed, and a return value of `NO` means that some error was encountered. If you use the `-[FMDatabase executeUpdate:error:withArgumentsInArray:orVAList:]` method to execute an update, you may supply an `NSError **` that will be filled in if execution fails. Otherwise you may invoke the `-lastErrorMessage` and `-lastErrorCode` methods to retrieve more information.

### Executing Queries

A `SELECT` statement is a query and is executed via one of the `-executeQuery...` methods.

Executing queries returns an `FMResultSet` object if successful, and `nil` upon failure. Like executing updates, there is a variant that accepts an `NSError **` parameter. Otherwise you should use the `-lastErrorMessage` and `-lastErrorCode` methods to determine why a query failed.

In order to iterate through the results of your query, you use a `while()` loop. You also need to "step" from one record to the other. With FMDB, the easiest way to do that is like this:

FMResultSet *s = [db executeQuery:@"SELECT * FROM myTable"];
while ([s next]) {
//retrieve values for each record
}

You must always invoke `-[FMResultSet next]` before attempting to access the values returned in a query, even if you're only expecting one:

FMResultSet *s = [db executeQuery:@"SELECT COUNT(*) FROM myTable"];
if ([s next]) {
int totalCount = [s intForColumnIndex:0];
}

`FMResultSet` has many methods to retrieve data in an appropriate format:

- `intForColumn:`
- `longForColumn:`
- `longLongIntForColumn:`
- `boolForColumn:`
- `doubleForColumn:`
- `stringForColumn:`
- `dateForColumn:`
- `dataForColumn:`
- `dataNoCopyForColumn:`
- `UTF8StringForColumnIndex:`
- `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.

Typically, there's no need to `-close` an `FMResultSet` yourself, since that happens when either the result set is deallocated, or the parent database is closed.

### Closing

When you have finished executing queries and updates on the database, you should `-close` the `FMDatabase` connection so that SQLite will relinquish any resources it has acquired during the course of its operation.

[db close];

### Transactions

`FMDatabase` can begin and commit a transaction by invoking one of the appropriate methods or executing a begin/end transaction statement.

### Data Sanitization

When providing a SQL statement to FMDB, you should not attempt to "sanitize" any values before insertion. Instead, you should use the standard SQLite binding syntax:

INSERT INTO myTable VALUES (?, ?, ?)

The `?` character is recognized by SQLite as a placeholder for a value to be inserted. The execution methods all accept a variable number of arguments (or a representation of those arguments, such as an `NSArray` or a `va_list`), which are properly escaped for you.

Thus, you SHOULD NOT do this (or anything like this):

[db executeUpdate:[NSString stringWithFormat:@"INSERT INTO myTable VALUES (%@)", @"this has \" lots of ' bizarre \" quotes '"]];

Instead, you SHOULD do:

[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @"this has \" lots of ' bizarre \" quotes '"];

All arguments provided to the `-executeUpdate:` method (or any of the variants that accept a `va_list` as a parameter) must be objects. The following will not work (and will result in a crash):

[db executeUpdate:@"INSERT INTO myTable VALUES (?)", 42];

The proper way to insert a number is to box it in an `NSNumber` object:

[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:42]];

Alternatively, you can use the `-execute*WithFormat:` variant to use `NSString`-style substitution:

[db executeUpdateWithFormat:@"INSERT INTO myTable VALUES (%d)", 42];

Internally, the `-execute*WithFormat:` methods are properly boxing things for you. The following percent modifiers are recognized: `%@`, `%c`, `%s`, `%d`, `%D`, `%i`, `%u`, `%U`, `%hi`, `%hu`, `%qi`, `%qu`, `%f`, `%g`, `%ld`, `%lu`, `%lld`, and `%llu`. Using a modifier other than those will have unpredictable results. If, for some reason, you need the `%` character to appear in your SQL statement, you should use `%%`.

## History

The history and changes are availbe on its [GitHub page](https://github.com/ccgus/fmdb) and are summarized in the "CHANGES_AND_TODO_LIST.txt" file.

## Contributors

The contributors to FMDB are contained in the "Contributors.txt" file.

## License

The license for FMDB is contained in the "License.txt" file.
2 changes: 2 additions & 0 deletions src/FMDatabase.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@

- (BOOL)update:(NSString*)sql error:(NSError**)outErr bind:(id)bindArgs, ...;
- (BOOL)executeUpdate:(NSString*)sql, ...;
- (BOOL)executeUpdateWithFormat:(NSString *)format, ...;
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments;
- (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args; // you shouldn't ever need to call this. use the previous two instead.

- (FMResultSet *)executeQuery:(NSString*)sql, ...;
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...;
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments;
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args; // you shouldn't ever need to call this. use the previous two instead.

Expand Down
120 changes: 120 additions & 0 deletions src/FMDatabase.m
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,100 @@ - (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt {
}
}

- (void)_extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments {
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':
arg = [NSNumber numberWithChar:va_arg(args, char)]; 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') {
arg = [NSNumber numberWithShort:va_arg(args, short)];
} else if (i < length && [sql characterAtIndex:i] == 'u') {
arg = [NSNumber numberWithUnsignedShort:va_arg(args, unsigned short)];
} 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':
arg = [NSNumber numberWithFloat:va_arg(args, float)]; 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 != '\0') {
[cleanedSQL appendFormat:@"%C", add];
}
last = current;
}

}

- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args {

if (inUse) {
Expand Down Expand Up @@ -428,6 +522,19 @@ - (FMResultSet *)executeQuery:(NSString*)sql, ... {
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 orVAList:nil];
}
Expand Down Expand Up @@ -623,6 +730,19 @@ - (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments {
return [self executeUpdate:sql error:nil withArgumentsInArray: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 error:(NSError**)outErr bind:(id)bindArgs, ... {
va_list args;
va_start(args, bindArgs);
Expand Down
2 changes: 2 additions & 0 deletions src/FMDatabaseAdditions.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@
- (FMResultSet*)getTableSchema:(NSString*)tableName;
- (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName;

- (BOOL)validateSQL:(NSString*)sql error:(NSError**)error;

@end
34 changes: 34 additions & 0 deletions src/FMDatabaseAdditions.m
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,38 @@ - (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName {
return returnBool;
}

- (BOOL)validateSQL:(NSString*)sql error:(NSError**)error {
sqlite3_stmt *pStmt = NULL;
BOOL validationSucceeded = YES;
BOOL keepTrying = YES;
int numberOfRetries = 0;

[self setInUse:YES];
while (keepTrying == YES) {
keepTrying = NO;
int rc = sqlite3_prepare_v2(db, [sql UTF8String], -1, &pStmt, 0);
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]];
}
}
}
[self setInUse:NO];
sqlite3_finalize(pStmt);

return validationSucceeded;
}

@end
5 changes: 3 additions & 2 deletions src/FMResultSet.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
- (BOOL)next;
- (BOOL)hasAnotherRow;

- (int)numberOfColumns;

- (int)columnIndexForName:(NSString*)columnName;
- (NSString*)columnNameForIndex:(int)columnIdx;

Expand Down Expand Up @@ -72,8 +74,8 @@
- (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName;

// returns one of NSNumber, NSString, NSData, or NSNull
- (id)objectForColumn:(NSString*)columnName;
- (id)objectForColumnIndex:(int)columnIdx;
- (id)objectForColumnName:(NSString*)columnName;

/*
If you are going to use this data after you iterate over the next row, or after you close the
Expand All @@ -83,7 +85,6 @@ If you don't, you're going to be in a world of hurt when you try and use the dat
- (NSData*)dataNoCopyForColumn:(NSString*)columnName NS_RETURNS_NOT_RETAINED;
- (NSData*)dataNoCopyForColumnIndex:(int)columnIdx NS_RETURNS_NOT_RETAINED;


- (BOOL)columnIndexIsNull:(int)columnIdx;
- (BOOL)columnIsNull:(NSString*)columnName;

Expand Down
6 changes: 5 additions & 1 deletion src/FMResultSet.m
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ - (void)close {
parentDB = nil;
}

- (int)numberOfColumns {
return sqlite3_column_count(statement.statement);
}

- (void)setupColumnNames {

if (!columnNameToIndexMap) {
Expand Down Expand Up @@ -354,7 +358,7 @@ - (id)objectForColumnIndex:(int)columnIdx {
return returnValue;
}

- (id)objectForColumnName:(NSString*)columnName {
- (id)objectForColumn:(NSString*)columnName {
return [self objectForColumnIndex:[self columnIndexForName:columnName]];
}

Expand Down

0 comments on commit c2a13ff

Please sign in to comment.