Browse Source

Merge pull request #11 from davedelong/master.

FMDB: now with more documentation!
ccgus 14 years ago
parent
commit
c2a13fffeb
8 changed files with 295 additions and 6 deletions
  1. 6 0
      CHANGES_AND_TODO_LIST.txt
  2. 123 3
      README.markdown
  3. 2 0
      src/FMDatabase.h
  4. 120 0
      src/FMDatabase.m
  5. 2 0
      src/FMDatabaseAdditions.h
  6. 34 0
      src/FMDatabaseAdditions.m
  7. 3 2
      src/FMResultSet.h
  8. 5 1
      src/FMResultSet.m

+ 6 - 0
CHANGES_AND_TODO_LIST.txt

@@ -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.

+ 123 - 3
README.markdown

@@ -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 - 0
src/FMDatabase.h

@@ -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.
 

+ 120 - 0
src/FMDatabase.m

@@ -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) {
@@ -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];
 }
@@ -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);

+ 2 - 0
src/FMDatabaseAdditions.h

@@ -28,4 +28,6 @@
 - (FMResultSet*)getTableSchema:(NSString*)tableName;
 - (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName;
 
+- (BOOL)validateSQL:(NSString*)sql error:(NSError**)error;
+
 @end

+ 34 - 0
src/FMDatabaseAdditions.m

@@ -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

+ 3 - 2
src/FMResultSet.h

@@ -41,6 +41,8 @@
 - (BOOL)next;
 - (BOOL)hasAnotherRow;
 
+- (int)numberOfColumns;
+
 - (int)columnIndexForName:(NSString*)columnName;
 - (NSString*)columnNameForIndex:(int)columnIdx;
 
@@ -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
@@ -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;
 

+ 5 - 1
src/FMResultSet.m

@@ -52,6 +52,10 @@ - (void)close {
     parentDB = nil;
 }
 
+- (int)numberOfColumns {
+	return sqlite3_column_count(statement.statement);
+}
+
 - (void)setupColumnNames {
     
     if (!columnNameToIndexMap) {
@@ -354,7 +358,7 @@ - (id)objectForColumnIndex:(int)columnIdx {
     return returnValue;
 }
 
-- (id)objectForColumnName:(NSString*)columnName {
+- (id)objectForColumn:(NSString*)columnName {
     return [self objectForColumnIndex:[self columnIndexForName:columnName]];
 }