Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/ccgus/fmdb.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Ryan <robert.ryan@mindspring.com>2020-05-06 19:48:18 +0300
committerRobert Ryan <robert.ryan@mindspring.com>2020-05-06 20:09:11 +0300
commit4009be0a6f9de7cc6d6a59cf8ff820269015a828 (patch)
tree5d0b3b0a843c9c1732a709ab47560dafbc850a2b
parentdaa532443f7ef8b441a8c886c80f113ac433ca92 (diff)
Allow rebinding of values for prepared queries
-rw-r--r--CHANGES_AND_TODO_LIST.txt5
-rw-r--r--Tests/FMDatabaseTests.m186
-rw-r--r--src/fmdb/FMDatabase.h6
-rw-r--r--src/fmdb/FMDatabase.m426
-rw-r--r--src/fmdb/FMDatabaseAdditions.m4
-rw-r--r--src/fmdb/FMResultSet.h43
-rw-r--r--src/fmdb/FMResultSet.m51
7 files changed, 407 insertions, 314 deletions
diff --git a/CHANGES_AND_TODO_LIST.txt b/CHANGES_AND_TODO_LIST.txt
index d644967..0783142 100644
--- a/CHANGES_AND_TODO_LIST.txt
+++ b/CHANGES_AND_TODO_LIST.txt
@@ -3,8 +3,11 @@ 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 the necessary of tests for your new code to tests target. 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.
+2020.05.06 Version 2.7.7
+ Add `prepare` and `bind` methods so you can prepare a statement once and bind values repeatedly.
+
2020.04.23 Version 2.7.6
-A new tag for the Swift Package Manager.
+ A new tag for the Swift Package Manager.
2018.10.23 Version 2.7.5
Xcode 10 support. Probably some other stuff over the past year as well.
diff --git a/Tests/FMDatabaseTests.m b/Tests/FMDatabaseTests.m
index b9e1f8a..8ca2df5 100644
--- a/Tests/FMDatabaseTests.m
+++ b/Tests/FMDatabaseTests.m
@@ -1124,7 +1124,12 @@
}
- (void)testVersionNumber {
- XCTAssertEqual([FMDatabase FMDBVersion], 0x0276); // this is going to break everytime we bump it.
+ XCTAssertEqual([FMDatabase FMDBVersion], 0x0277); // this is going to break everytime we bump it.
+}
+
+- (void)testUserVersion {
+ NSComparisonResult result = [[FMDatabase FMDBUserVersion] compare:@"2.7.7" options:NSNumericSearch];
+ XCTAssertEqual(result, NSOrderedSame);
}
- (void)testVersionStringAboveRequired {
@@ -1551,4 +1556,183 @@
[manager removeItemAtURL:fileURL2 error:nil];
}
+// These three utility methods used by `testTransient`, to illustrate dangers of SQLITE_STATIC
+
+- (BOOL)utility1ForTestTransient:(FMDatabase *)db withValue:(long)value {
+ @autoreleasepool {
+ NSString *string = [[NSString alloc] initWithFormat:@"value %@", @(value)];
+ return [db executeUpdate:@"INSERT INTO foo (bar) VALUES (?)", [string dataUsingEncoding:NSUTF8StringEncoding]];
+ }
+}
+
+- (FMResultSet *)utility2ForTestTransient:(FMDatabase *)db withValue:(long)value {
+ @autoreleasepool {
+ NSString *string = [[NSString alloc] initWithFormat:@"value %@", @(value)];
+ return [db executeQuery:@"SELECT * FROM foo WHERE bar = ?", [string dataUsingEncoding:NSUTF8StringEncoding]];
+ }
+}
+
+- (BOOL)utility3ForTestTransient:(FMResultSet *)rs withValue:(long)value {
+ @autoreleasepool {
+ NSString *string = [[NSString alloc] initWithFormat:@"xxxxx %@", @(value + 1)];
+ XCTAssertEqualObjects(string, @"xxxxx 43"); // Just to ensure the above isn't optimized out
+ return [rs next];
+ }
+}
+
+- (void)testTransient {
+ NSURL *tempURL = [NSURL fileURLWithPath:NSTemporaryDirectory()];
+ NSURL *fileURL = [tempURL URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
+ NSFileManager *manager = [NSFileManager defaultManager];
+
+ // ok, first create one database
+
+ FMDatabase *db = [FMDatabase databaseWithURL:fileURL];
+ BOOL success = [db open];
+ XCTAssert(success, @"Database not created correctly for purposes of test");
+ success = [db executeUpdate:@"CREATE TABLE IF NOT EXISTS foo (bar BLOB)"];
+ XCTAssert(success, @"Table created correctly for purposes of test");
+
+ long value = 42;
+ success = [self utility1ForTestTransient:db withValue:value];
+ XCTAssert(success, @"INSERT failed");
+
+ FMResultSet *rs = [self utility2ForTestTransient:db withValue:value];
+ XCTAssert(rs, @"Creating SELECT failed");
+
+ // the following is the key test, namely if FMDB uses SQLITE_STATIC, the following may fail, but SQLITE_TRANSIENT ensures it will succeed
+
+ success = [self utility3ForTestTransient:rs withValue:value];
+ XCTAssert(success, @"Performing SELECT failed");
+
+ // let's clean up
+
+ [rs close];
+ [db close];
+ [manager removeItemAtURL:fileURL error:nil];
+}
+
+- (void)testBindFailure {
+ NSURL *tempURL = [NSURL fileURLWithPath:NSTemporaryDirectory()];
+ NSURL *fileURL = [tempURL URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
+ NSFileManager *manager = [NSFileManager defaultManager];
+
+ // ok, first create one database
+
+ FMDatabase *db = [FMDatabase databaseWithURL:fileURL];
+ BOOL success = [db open];
+ XCTAssert(success, @"Database not created correctly for purposes of test");
+ success = [db executeUpdate:@"CREATE TABLE IF NOT EXISTS foo (bar BLOB)"];
+ XCTAssert(success, @"Table created correctly for purposes of test");
+
+ NSUInteger limit = (NSUInteger)[db limitFor:SQLITE_LIMIT_LENGTH value:-1] + 1;
+ NSLog(@"%lu", (unsigned long)limit);
+ NSData *data = [NSMutableData dataWithLength:limit];
+ success = [db executeUpdate:@"INSERT INTO foo (bar) VALUES (?)", data];
+ XCTAssertFalse(success, @"Table created correctly for purposes of test");
+
+ // let's clean up
+
+ [db close];
+ [manager removeItemAtURL:fileURL error:nil];
+}
+
+- (void)testRebindingWithDictionary {
+ NSURL *tempURL = [NSURL fileURLWithPath:NSTemporaryDirectory()];
+ NSURL *fileURL = [tempURL URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
+ NSFileManager *manager = [NSFileManager defaultManager];
+ [manager removeItemAtURL:fileURL error:nil];
+
+ // ok, first create one database
+
+ FMDatabase *db = [FMDatabase databaseWithURL:fileURL];
+ BOOL success = [db open];
+ XCTAssert(success, @"Database not created correctly for purposes of test");
+ success = [db executeUpdate:@"CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY, bar TEXT)"];
+ XCTAssert(success, @"Table created correctly for purposes of test");
+
+ FMResultSet *rs = [db prepare:@"INSERT INTO foo (bar) VALUES (:bar)"];
+ XCTAssert(rs, @"INSERT statement not prepared %@", [db lastErrorMessage]);
+
+ NSString *value1 = @"foo";
+ XCTAssert([rs bindWithDictionary:@{@"bar": value1}], @"Unable to bind");
+ XCTAssert([rs step], @"Performing query failed");
+
+ NSString *value2 = @"bar";
+ XCTAssert([rs bindWithDictionary:@{@"bar": value2}], @"Unable to bind");
+ XCTAssert([rs step], @"Performing query failed");
+
+ XCTAssert([rs bindWithDictionary:@{@"bar": value2}], @"Unable to bind");
+ XCTAssert([rs step], @"Performing query failed");
+
+ [rs close];
+
+ rs = [db prepare:@"SELECT bar FROM foo WHERE bar = :bar"];
+ XCTAssert([rs bindWithDictionary:@{@"bar": value1}], @"Unable to bind");
+ XCTAssert([rs next], @"No record found");
+ XCTAssertEqualObjects([rs stringForColumnIndex:0], value1);
+ XCTAssertFalse([rs next], @"There should have been only one record");
+
+ XCTAssert([rs bindWithDictionary:@{@"bar": value2}], @"Unable to bind");
+ XCTAssert([rs next], @"No record found");
+ XCTAssertEqualObjects([rs stringForColumnIndex:0], value2);
+ XCTAssert([rs next], @"No record found");
+ XCTAssertEqualObjects([rs stringForColumnIndex:0], value2);
+ XCTAssertFalse([rs next], @"There should have been only two records");
+
+ // let's clean up
+
+ [rs close];
+ [db close];
+ [manager removeItemAtURL:fileURL error:nil];
+}
+
+- (void)testRebindingWithArray {
+ NSURL *tempURL = [NSURL fileURLWithPath:NSTemporaryDirectory()];
+ NSURL *fileURL = [tempURL URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
+ NSFileManager *manager = [NSFileManager defaultManager];
+
+ // ok, first create one database
+
+ FMDatabase *db = [FMDatabase databaseWithURL:fileURL];
+ BOOL success = [db open];
+ XCTAssert(success, @"Database not created correctly for purposes of test");
+ success = [db executeUpdate:@"CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY, bar TEXT)"];
+ XCTAssert(success, @"Table created correctly for purposes of test");
+
+ FMResultSet *rs = [db prepare:@"INSERT INTO foo (bar) VALUES (?)"];
+ XCTAssert(rs, @"INSERT statement not prepared %@", [db lastErrorMessage]);
+
+ NSString *value1 = @"foo";
+ XCTAssert([rs bindWithArray:@[value1]], @"Unable to bind");
+ XCTAssert([rs step], @"Performing INSERT 1 failed");
+
+ NSString *value2 = @"bar";
+ XCTAssert([rs bindWithArray:@[value2]], @"Unable to bind");
+ XCTAssert([rs step], @"Performing INSERT 2 failed");
+ XCTAssert([rs bindWithArray:@[value2]], @"Unable to bind");
+ XCTAssert([rs step], @"Performing INSERT 2 failed");
+
+ [rs close];
+
+ rs = [db prepare:@"SELECT bar FROM foo WHERE bar = ?"];
+ XCTAssert([rs bindWithArray:@[value1]], @"Unable to bind");
+ XCTAssert([rs next], @"No record found");
+ XCTAssertEqualObjects([rs stringForColumnIndex:0], value1);
+ XCTAssertFalse([rs next], @"There should have been only one record");
+
+ XCTAssert([rs bindWithArray:@[value2]], @"Unable to bind");
+ XCTAssert([rs next], @"No record found");
+ XCTAssertEqualObjects([rs stringForColumnIndex:0], value2);
+ XCTAssert([rs next], @"No record found");
+ XCTAssertEqualObjects([rs stringForColumnIndex:0], value2);
+ XCTAssertFalse([rs next], @"There should have been only two records");
+
+ // let's clean up
+
+ [rs close];
+ [db close];
+ [manager removeItemAtURL:fileURL error:nil];
+}
+
@end
diff --git a/src/fmdb/FMDatabase.h b/src/fmdb/FMDatabase.h
index 35a3e90..472def9 100644
--- a/src/fmdb/FMDatabase.h
+++ b/src/fmdb/FMDatabase.h
@@ -677,6 +677,12 @@ typedef NS_ENUM(int, FMDBCheckpointMode) {
// Documentation forthcoming.
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withVAList:(va_list)args;
+/// Prepare SQL statement.
+///
+/// @param sql SQL statement to prepare, generally with `?` placeholders.
+
+- (FMResultSet *)prepare:(NSString *)sql;
+
///-------------------
/// @name Transactions
///-------------------
diff --git a/src/fmdb/FMDatabase.m b/src/fmdb/FMDatabase.m
index 49fffc2..7c1eab6 100644
--- a/src/fmdb/FMDatabase.m
+++ b/src/fmdb/FMDatabase.m
@@ -8,6 +8,10 @@
#import <sqlite3.h>
#endif
+// MARK: - FMDatabase Private Extension
+
+NS_ASSUME_NONNULL_BEGIN
+
@interface FMDatabase () {
void* _db;
BOOL _isExecutingStatement;
@@ -19,15 +23,23 @@
NSDateFormatter *_dateFormat;
}
-NS_ASSUME_NONNULL_BEGIN
-
-- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray * _Nullable)arrayArgs orDictionary:(NSDictionary * _Nullable)dictionaryArgs orVAList:(va_list)args;
+- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray * _Nullable)arrayArgs orDictionary:(NSDictionary * _Nullable)dictionaryArgs orVAList:(va_list)args shouldBind:(BOOL)shouldBind;
- (BOOL)executeUpdate:(NSString *)sql error:(NSError * _Nullable __autoreleasing *)outErr withArgumentsInArray:(NSArray * _Nullable)arrayArgs orDictionary:(NSDictionary * _Nullable)dictionaryArgs orVAList:(va_list)args;
-NS_ASSUME_NONNULL_END
+@end
+
+// MARK: - FMResultSet Private Extension
+
+@interface FMResultSet ()
+
+- (int)internalStepWithError:(NSError * _Nullable __autoreleasing *)outErr;
@end
+NS_ASSUME_NONNULL_END
+
+// MARK: - FMDatabase
+
@implementation FMDatabase
// Because these two properties have all of their accessor methods implemented,
@@ -99,7 +111,7 @@ NS_ASSUME_NONNULL_END
}
+ (NSString*)FMDBUserVersion {
- return @"2.7.6";
+ return @"2.7.7";
}
+ (SInt32)FMDBVersion {
@@ -158,6 +170,10 @@ NS_ASSUME_NONNULL_END
}
+- (int)limitFor:(int)type value:(int)newLimit {
+ return sqlite3_limit(_db, type, newLimit);
+}
+
#pragma mark Open and close database
- (BOOL)open {
@@ -250,6 +266,7 @@ NS_ASSUME_NONNULL_END
while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) {
NSLog(@"Closing leaked statement");
sqlite3_finalize(pStmt);
+ pStmt = 0x00;
retry = YES;
}
}
@@ -619,10 +636,10 @@ static int FMDBDatabaseBusyHandler(void *f, int count) {
#pragma mark SQL manipulation
-- (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt {
+- (int)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt {
if ((!obj) || ((NSNull *)obj == [NSNull null])) {
- sqlite3_bind_null(pStmt, idx);
+ return sqlite3_bind_null(pStmt, idx);
}
// FIXME - someday check the return codes on these binds.
@@ -633,62 +650,61 @@ static int FMDBDatabaseBusyHandler(void *f, int count) {
// Don't pass a NULL pointer, or sqlite will bind a SQL null instead of a blob.
bytes = "";
}
- sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_STATIC);
+ return sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_TRANSIENT);
}
else if ([obj isKindOfClass:[NSDate class]]) {
if (self.hasDateFormatter)
- sqlite3_bind_text(pStmt, idx, [[self stringFromDate:obj] UTF8String], -1, SQLITE_STATIC);
+ return sqlite3_bind_text(pStmt, idx, [[self stringFromDate:obj] UTF8String], -1, SQLITE_TRANSIENT);
else
- sqlite3_bind_double(pStmt, idx, [obj timeIntervalSince1970]);
+ return sqlite3_bind_double(pStmt, idx, [obj timeIntervalSince1970]);
}
else if ([obj isKindOfClass:[NSNumber class]]) {
if (strcmp([obj objCType], @encode(char)) == 0) {
- sqlite3_bind_int(pStmt, idx, [obj charValue]);
+ return sqlite3_bind_int(pStmt, idx, [obj charValue]);
}
else if (strcmp([obj objCType], @encode(unsigned char)) == 0) {
- sqlite3_bind_int(pStmt, idx, [obj unsignedCharValue]);
+ return sqlite3_bind_int(pStmt, idx, [obj unsignedCharValue]);
}
else if (strcmp([obj objCType], @encode(short)) == 0) {
- sqlite3_bind_int(pStmt, idx, [obj shortValue]);
+ return sqlite3_bind_int(pStmt, idx, [obj shortValue]);
}
else if (strcmp([obj objCType], @encode(unsigned short)) == 0) {
- sqlite3_bind_int(pStmt, idx, [obj unsignedShortValue]);
+ return sqlite3_bind_int(pStmt, idx, [obj unsignedShortValue]);
}
else if (strcmp([obj objCType], @encode(int)) == 0) {
- sqlite3_bind_int(pStmt, idx, [obj intValue]);
+ return sqlite3_bind_int(pStmt, idx, [obj intValue]);
}
else if (strcmp([obj objCType], @encode(unsigned int)) == 0) {
- sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedIntValue]);
+ return sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedIntValue]);
}
else if (strcmp([obj objCType], @encode(long)) == 0) {
- sqlite3_bind_int64(pStmt, idx, [obj longValue]);
+ return sqlite3_bind_int64(pStmt, idx, [obj longValue]);
}
else if (strcmp([obj objCType], @encode(unsigned long)) == 0) {
- sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongValue]);
+ return sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongValue]);
}
else if (strcmp([obj objCType], @encode(long long)) == 0) {
- sqlite3_bind_int64(pStmt, idx, [obj longLongValue]);
+ return sqlite3_bind_int64(pStmt, idx, [obj longLongValue]);
}
else if (strcmp([obj objCType], @encode(unsigned long long)) == 0) {
- sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongLongValue]);
+ return sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongLongValue]);
}
else if (strcmp([obj objCType], @encode(float)) == 0) {
- sqlite3_bind_double(pStmt, idx, [obj floatValue]);
+ return sqlite3_bind_double(pStmt, idx, [obj floatValue]);
}
else if (strcmp([obj objCType], @encode(double)) == 0) {
- sqlite3_bind_double(pStmt, idx, [obj doubleValue]);
+ return sqlite3_bind_double(pStmt, idx, [obj doubleValue]);
}
else if (strcmp([obj objCType], @encode(BOOL)) == 0) {
- sqlite3_bind_int(pStmt, idx, ([obj boolValue] ? 1 : 0));
+ return sqlite3_bind_int(pStmt, idx, ([obj boolValue] ? 1 : 0));
}
else {
- sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
+ return sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_TRANSIENT);
}
}
- else {
- sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
- }
+
+ return sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_TRANSIENT);
}
- (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments {
@@ -814,11 +830,10 @@ static int FMDBDatabaseBusyHandler(void *f, int count) {
#pragma mark Execute queries
- (FMResultSet *)executeQuery:(NSString *)sql withParameterDictionary:(NSDictionary *)arguments {
- return [self executeQuery:sql withArgumentsInArray:nil orDictionary:arguments orVAList:nil];
+ return [self executeQuery:sql withArgumentsInArray:nil orDictionary:arguments orVAList:nil shouldBind:true];
}
-- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {
-
+- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args shouldBind:(BOOL)shouldBind {
if (![self databaseExists]) {
return 0x00;
}
@@ -846,7 +861,6 @@ static int FMDBDatabaseBusyHandler(void *f, int count) {
}
if (!pStmt) {
-
rc = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0);
if (SQLITE_OK != rc) {
@@ -862,35 +876,79 @@ static int FMDBDatabaseBusyHandler(void *f, int count) {
}
sqlite3_finalize(pStmt);
+ pStmt = 0x00;
_isExecutingStatement = NO;
return nil;
}
}
+
+ if (shouldBind) {
+ BOOL success = [self bindStatement:pStmt WithArgumentsInArray:arrayArgs orDictionary:dictionaryArgs orVAList:args];
+ if (!success) {
+ return nil;
+ }
+ }
+
+ FMDBRetain(statement); // to balance the release below
+ if (!statement) {
+ statement = [[FMStatement alloc] init];
+ [statement setStatement:pStmt];
+
+ if (_shouldCacheStatements && sql) {
+ [self setCachedStatement:statement forQuery:sql];
+ }
+ }
+
+ // the statement gets closed in rs's dealloc or [rs close];
+ // we should only autoclose if we're binding automatically when the statement is prepared
+ rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self shouldAutoClose:shouldBind];
+ [rs setQuery:sql];
+
+ NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
+ [_openResultSets addObject:openResultSet];
+
+ [statement setUseCount:[statement useCount] + 1];
+
+ FMDBRelease(statement);
+
+ _isExecutingStatement = NO;
+
+ return rs;
+}
+
+- (BOOL)bindStatement:(sqlite3_stmt *)pStmt WithArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {
id obj;
int idx = 0;
int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)
-
+
// If dictionaryArgs is passed in, that means we are using sqlite's named parameter support
if (dictionaryArgs) {
-
+
for (NSString *dictionaryKey in [dictionaryArgs allKeys]) {
-
+
// Prefix the key with a colon.
NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey];
-
+
if (_traceExecution) {
NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]);
}
-
+
// Get the index for the parameter name.
int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]);
-
+
FMDBRelease(parameterName);
-
+
if (namedIdx > 0) {
// Standard binding from here.
- [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
+ int rc = [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
+ if (rc != SQLITE_OK) {
+ NSLog(@"Error: unable to bind (%d, %s", rc, sqlite3_errmsg(_db));
+ sqlite3_finalize(pStmt);
+ pStmt = 0x00;
+ _isExecutingStatement = NO;
+ return false;
+ }
// increment the binding count, so our check below works out
idx++;
}
@@ -900,9 +958,7 @@ static int FMDBDatabaseBusyHandler(void *f, int count) {
}
}
else {
-
while (idx < queryCount) {
-
if (arrayArgs && idx < (int)[arrayArgs count]) {
obj = [arrayArgs objectAtIndex:(NSUInteger)idx];
}
@@ -913,7 +969,7 @@ static int FMDBDatabaseBusyHandler(void *f, int count) {
//We ran out of arguments
break;
}
-
+
if (_traceExecution) {
if ([obj isKindOfClass:[NSData class]]) {
NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]);
@@ -922,52 +978,36 @@ static int FMDBDatabaseBusyHandler(void *f, int count) {
NSLog(@"obj: %@", obj);
}
}
-
+
idx++;
-
- [self bindObject:obj toColumn:idx inStatement:pStmt];
+
+ int rc = [self bindObject:obj toColumn:idx inStatement:pStmt];
+ if (rc != SQLITE_OK) {
+ NSLog(@"Error: unable to bind (%d, %s", rc, sqlite3_errmsg(_db));
+ sqlite3_finalize(pStmt);
+ pStmt = 0x00;
+ _isExecutingStatement = NO;
+ return false;
+ }
}
}
-
+
if (idx != queryCount) {
NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)");
sqlite3_finalize(pStmt);
+ pStmt = 0x00;
_isExecutingStatement = NO;
- return nil;
+ return false;
}
-
- FMDBRetain(statement); // to balance the release below
-
- if (!statement) {
- statement = [[FMStatement alloc] init];
- [statement setStatement:pStmt];
-
- if (_shouldCacheStatements && sql) {
- [self setCachedStatement:statement forQuery:sql];
- }
- }
-
- // the statement gets closed in rs's dealloc or [rs close];
- rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];
- [rs setQuery:sql];
-
- NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
- [_openResultSets addObject:openResultSet];
-
- [statement setUseCount:[statement useCount] + 1];
-
- FMDBRelease(statement);
-
- _isExecutingStatement = NO;
-
- return rs;
+
+ return true;
}
- (FMResultSet *)executeQuery:(NSString*)sql, ... {
va_list args;
va_start(args, sql);
- id result = [self executeQuery:sql withArgumentsInArray:nil orDictionary:nil orVAList:args];
+ id result = [self executeQuery:sql withArgumentsInArray:nil orDictionary:nil orVAList:args shouldBind:true];
va_end(args);
return result;
@@ -987,11 +1027,11 @@ static int FMDBDatabaseBusyHandler(void *f, int count) {
}
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments {
- return [self executeQuery:sql withArgumentsInArray:arguments orDictionary:nil orVAList:nil];
+ return [self executeQuery:sql withArgumentsInArray:arguments orDictionary:nil orVAList:nil shouldBind:true];
}
- (FMResultSet *)executeQuery:(NSString *)sql values:(NSArray *)values error:(NSError * __autoreleasing *)error {
- FMResultSet *rs = [self executeQuery:sql withArgumentsInArray:values orDictionary:nil orVAList:nil];
+ FMResultSet *rs = [self executeQuery:sql withArgumentsInArray:values orDictionary:nil orVAList:nil shouldBind:true];
if (!rs && error) {
*error = [self lastError];
}
@@ -999,234 +1039,22 @@ static int FMDBDatabaseBusyHandler(void *f, int count) {
}
- (FMResultSet *)executeQuery:(NSString*)sql withVAList:(va_list)args {
- return [self executeQuery:sql withArgumentsInArray:nil orDictionary:nil orVAList:args];
+ return [self executeQuery:sql withArgumentsInArray:nil orDictionary:nil orVAList:args shouldBind:true];
}
#pragma mark Execute updates
- (BOOL)executeUpdate:(NSString*)sql error:(NSError * _Nullable __autoreleasing *)outErr withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {
-
- if (![self databaseExists]) {
- return NO;
- }
-
- if (_isExecutingStatement) {
- [self warnInUse];
- return NO;
- }
-
- _isExecutingStatement = YES;
-
- int rc = 0x00;
- sqlite3_stmt *pStmt = 0x00;
- FMStatement *cachedStmt = 0x00;
-
- if (_traceExecution && sql) {
- NSLog(@"%@ executeUpdate: %@", self, sql);
- }
-
- if (_shouldCacheStatements) {
- cachedStmt = [self cachedStatementForQuery:sql];
- pStmt = cachedStmt ? [cachedStmt statement] : 0x00;
- [cachedStmt reset];
- }
-
- if (!pStmt) {
- rc = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0);
-
- if (SQLITE_OK != rc) {
- if (_logsErrors) {
- NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
- NSLog(@"DB Query: %@", sql);
- NSLog(@"DB Path: %@", _databasePath);
- }
-
- if (_crashOnErrors) {
- NSAssert(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
- abort();
- }
-
- if (outErr) {
- *outErr = [self errorWithMessage:[NSString stringWithUTF8String:sqlite3_errmsg(_db)]];
- }
-
- sqlite3_finalize(pStmt);
-
- _isExecutingStatement = NO;
- return NO;
- }
- }
-
- id obj;
- int idx = 0;
- int queryCount = sqlite3_bind_parameter_count(pStmt);
-
- // If dictionaryArgs is passed in, that means we are using sqlite's named parameter support
- if (dictionaryArgs) {
-
- for (NSString *dictionaryKey in [dictionaryArgs allKeys]) {
-
- // Prefix the key with a colon.
- NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey];
-
- if (_traceExecution) {
- NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]);
- }
- // Get the index for the parameter name.
- int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]);
-
- FMDBRelease(parameterName);
-
- if (namedIdx > 0) {
- // Standard binding from here.
- [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
-
- // increment the binding count, so our check below works out
- idx++;
- }
- else {
- NSString *message = [NSString stringWithFormat:@"Could not find index for %@", dictionaryKey];
-
- if (_logsErrors) {
- NSLog(@"%@", message);
- }
- if (outErr) {
- *outErr = [self errorWithMessage:message];
- }
- }
- }
- }
- else {
-
- while (idx < queryCount) {
-
- if (arrayArgs && idx < (int)[arrayArgs count]) {
- obj = [arrayArgs objectAtIndex:(NSUInteger)idx];
- }
- else if (args) {
- obj = va_arg(args, id);
- }
- else {
- //We ran out of arguments
- break;
- }
-
- if (_traceExecution) {
- if ([obj isKindOfClass:[NSData class]]) {
- NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]);
- }
- else {
- NSLog(@"obj: %@", obj);
- }
- }
-
- idx++;
-
- [self bindObject:obj toColumn:idx inStatement:pStmt];
- }
- }
-
-
- if (idx != queryCount) {
- NSString *message = [NSString stringWithFormat:@"Error: the bind count (%d) is not correct for the # of variables in the query (%d) (%@) (executeUpdate)", idx, queryCount, sql];
- if (_logsErrors) {
- NSLog(@"%@", message);
- }
+ FMResultSet *rs = [self executeQuery:sql withArgumentsInArray:arrayArgs orDictionary:dictionaryArgs orVAList:args shouldBind:true];
+ if (!rs) {
if (outErr) {
- *outErr = [self errorWithMessage:message];
+ *outErr = [self lastError];
}
-
- sqlite3_finalize(pStmt);
- _isExecutingStatement = NO;
- return NO;
+ return false;
}
-
- /* Call sqlite3_step() to run the virtual machine. Since the SQL being
- ** executed is not a SELECT statement, we assume no data will be returned.
- */
-
- rc = sqlite3_step(pStmt);
-
- if (SQLITE_DONE == rc) {
- // all is well, let's return.
- }
- else if (SQLITE_INTERRUPT == rc) {
- if (_logsErrors) {
- NSLog(@"Error calling sqlite3_step. Query was interrupted (%d: %s) SQLITE_INTERRUPT", rc, sqlite3_errmsg(_db));
- NSLog(@"DB Query: %@", sql);
- }
- }
- else if (rc == SQLITE_ROW) {
- NSString *message = [NSString stringWithFormat:@"A executeUpdate is being called with a query string '%@'", sql];
- if (_logsErrors) {
- NSLog(@"%@", message);
- NSLog(@"DB Query: %@", sql);
- }
- if (outErr) {
- *outErr = [self errorWithMessage:message];
- }
- }
- else {
- if (outErr) {
- *outErr = [self errorWithMessage:[NSString stringWithUTF8String:sqlite3_errmsg(_db)]];
- }
-
- if (SQLITE_ERROR == rc) {
- if (_logsErrors) {
- NSLog(@"Error calling sqlite3_step (%d: %s) SQLITE_ERROR", rc, sqlite3_errmsg(_db));
- NSLog(@"DB Query: %@", sql);
- }
- }
- else if (SQLITE_MISUSE == rc) {
- // uh oh.
- if (_logsErrors) {
- NSLog(@"Error calling sqlite3_step (%d: %s) SQLITE_MISUSE", rc, sqlite3_errmsg(_db));
- NSLog(@"DB Query: %@", sql);
- }
- }
- else {
- // wtf?
- if (_logsErrors) {
- NSLog(@"Unknown error calling sqlite3_step (%d: %s) eu", rc, sqlite3_errmsg(_db));
- NSLog(@"DB Query: %@", sql);
- }
- }
- }
-
- if (_shouldCacheStatements && !cachedStmt) {
- cachedStmt = [[FMStatement alloc] init];
-
- [cachedStmt setStatement:pStmt];
-
- [self setCachedStatement:cachedStmt forQuery:sql];
-
- FMDBRelease(cachedStmt);
- }
-
- int closeErrorCode;
-
- if (cachedStmt) {
- [cachedStmt setUseCount:[cachedStmt useCount] + 1];
- closeErrorCode = sqlite3_reset(pStmt);
- }
- else {
- /* Finalize the virtual machine. This releases all memory and other
- ** resources allocated by the sqlite3_prepare() call above.
- */
- closeErrorCode = sqlite3_finalize(pStmt);
- }
-
- if (closeErrorCode != SQLITE_OK) {
- if (_logsErrors) {
- NSLog(@"Unknown error finalizing or resetting statement (%d: %s)", closeErrorCode, sqlite3_errmsg(_db));
- NSLog(@"DB Query: %@", sql);
- }
- }
-
- _isExecutingStatement = NO;
- return (rc == SQLITE_DONE || rc == SQLITE_OK);
-}
+ return [rs internalStepWithError:outErr] == SQLITE_DONE;
+}
- (BOOL)executeUpdate:(NSString*)sql, ... {
va_list args;
@@ -1337,6 +1165,12 @@ int FMDBExecuteBulkSQLCallback(void *theBlockAsVoid, int columns, char **values,
#pragma clang diagnostic pop
+#pragma mark Prepare
+
+- (FMResultSet *)prepare:(NSString *)sql {
+ return [self executeQuery:sql withArgumentsInArray:nil orDictionary:nil orVAList:nil shouldBind:false];
+}
+
#pragma mark Transactions
- (BOOL)rollback {
@@ -1650,7 +1484,7 @@ void FMDBBlockSQLiteCallBackFunction(sqlite3_context *context, int argc, sqlite3
@end
-
+// MARK: - FMStatement
@implementation FMStatement
diff --git a/src/fmdb/FMDatabaseAdditions.m b/src/fmdb/FMDatabaseAdditions.m
index 24a0ebf..2289c13 100644
--- a/src/fmdb/FMDatabaseAdditions.m
+++ b/src/fmdb/FMDatabaseAdditions.m
@@ -17,7 +17,7 @@
#endif
@interface FMDatabase (PrivateStuff)
-- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray * _Nullable)arrayArgs orDictionary:(NSDictionary * _Nullable)dictionaryArgs orVAList:(va_list)args;
+- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray * _Nullable)arrayArgs orDictionary:(NSDictionary * _Nullable)dictionaryArgs orVAList:(va_list)args shouldBind:(BOOL)shouldBind;
@end
@implementation FMDatabase (FMDatabaseAdditions)
@@ -25,7 +25,7 @@
#define RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(type, sel) \
va_list args; \
va_start(args, query); \
-FMResultSet *resultSet = [self executeQuery:query withArgumentsInArray:0x00 orDictionary:0x00 orVAList:args]; \
+FMResultSet *resultSet = [self executeQuery:query withArgumentsInArray:0x00 orDictionary:0x00 orVAList:args shouldBind:true]; \
va_end(args); \
if (![resultSet next]) { return (type)0; } \
type ret = [resultSet sel:0]; \
diff --git a/src/fmdb/FMResultSet.h b/src/fmdb/FMResultSet.h
index b765b7a..b48dfd6 100644
--- a/src/fmdb/FMResultSet.h
+++ b/src/fmdb/FMResultSet.h
@@ -57,7 +57,7 @@ NS_ASSUME_NONNULL_BEGIN
@return A `FMResultSet` on success; `nil` on failure
*/
-+ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB;
++ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB shouldAutoClose:(BOOL)shouldAutoClose;
/** Close result set */
@@ -91,10 +91,30 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)nextWithError:(NSError * _Nullable __autoreleasing *)outErr;
+/** Perform SQL statement.
+
+ @return 'YES' if successful; 'NO' if not.
+
+ @see hasAnotherRow
+*/
+
+- (BOOL)step;
+
+/** Perform SQL statement.
+
+ @param outErr A 'NSError' object to receive any error object (if any).
+
+ @return 'YES' if successful; 'NO' if not.
+
+ @see hasAnotherRow
+*/
+
+- (BOOL)stepWithError:(NSError * _Nullable __autoreleasing *)outErr;
+
/** Did the last call to `<next>` succeed in retrieving another row?
- @return `YES` if the last call to `<next>` succeeded in retrieving another record; `NO` if not.
-
+ @return 'YES' if there is another row; 'NO' if not.
+
@see next
@warning The `hasAnotherRow` method must follow a call to `<next>`. If the previous database interaction was something other than a call to `next`, then this method may return `NO`, whether there is another row of data or not.
@@ -461,7 +481,22 @@ If you don't, you're going to be in a world of hurt when you try and use the dat
- (void)kvcMagic:(id)object;
-
+///-----------------------------
+/// @name Binding values
+///-----------------------------
+
+/// Bind array of values to prepared statement.
+///
+/// @param array Array of values to bind to SQL statement.
+
+- (BOOL)bindWithArray:(NSArray*)array;
+
+/// Bind dictionary of values to prepared statement.
+///
+/// @param dictionary Dictionary of values to bind to SQL statement.
+
+- (BOOL)bindWithDictionary:(NSDictionary *)dictionary;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/src/fmdb/FMResultSet.m b/src/fmdb/FMResultSet.m
index 4cb6525..1f32473 100644
--- a/src/fmdb/FMResultSet.m
+++ b/src/fmdb/FMResultSet.m
@@ -8,23 +8,31 @@
#import <sqlite3.h>
#endif
+// MARK: - FMDatabase Private Extension
+
@interface FMDatabase ()
- (void)resultSetDidClose:(FMResultSet *)resultSet;
+- (BOOL)bindStatement:(sqlite3_stmt *)pStmt WithArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args;
@end
+// MARK: - FMResultSet Private Extension
+
@interface FMResultSet () {
NSMutableDictionary *_columnNameToIndexMap;
}
+@property (nonatomic) BOOL shouldAutoClose;
@end
+// MARK: - FMResultSet
+
@implementation FMResultSet
-+ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB {
-
++ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB shouldAutoClose:(BOOL)shouldAutoClose {
FMResultSet *rs = [[FMResultSet alloc] init];
[rs setStatement:statement];
[rs setParentDB:aDB];
+ [rs setShouldAutoClose:shouldAutoClose];
NSParameterAssert(![statement inUse]);
[statement setInUse:YES]; // weak reference
@@ -153,15 +161,25 @@
return nil;
}
-
-
-
- (BOOL)next {
return [self nextWithError:nil];
}
- (BOOL)nextWithError:(NSError * _Nullable __autoreleasing *)outErr {
-
+ int rc = [self internalStepWithError:outErr];
+ return rc == SQLITE_ROW;
+}
+
+- (BOOL)step {
+ return [self stepWithError:nil];
+}
+
+- (BOOL)stepWithError:(NSError * _Nullable __autoreleasing *)outErr {
+ int rc = [self internalStepWithError:outErr];
+ return rc == SQLITE_DONE;
+}
+
+- (int)internalStepWithError:(NSError * _Nullable __autoreleasing *)outErr {
int rc = sqlite3_step([_statement statement]);
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
@@ -203,13 +221,12 @@
*outErr = [_parentDB lastError];
}
}
-
-
- if (rc != SQLITE_ROW) {
+
+ if (rc != SQLITE_ROW && _shouldAutoClose) {
[self close];
}
- return (rc == SQLITE_ROW);
+ return rc;
}
- (BOOL)hasAnotherRow {
@@ -428,5 +445,19 @@
return [self objectForColumn:columnName];
}
+// MARK: Bind
+
+- (BOOL)bindWithArray:(NSArray*)array orDictionary:(NSDictionary *)dictionary orVAList:(va_list)args {
+ [_statement reset];
+ return [_parentDB bindStatement:_statement.statement WithArgumentsInArray:array orDictionary:dictionary orVAList:args];
+}
+
+- (BOOL)bindWithArray:(NSArray*)array {
+ return [self bindWithArray:array orDictionary:nil orVAList:nil];
+}
+
+- (BOOL)bindWithDictionary:(NSDictionary *)dictionary {
+ return [self bindWithArray:nil orDictionary:dictionary orVAList:nil];
+}
@end