2  * Copyright (c) 2017 Apple Inc. All Rights Reserved.
 
   4  * @APPLE_LICENSE_HEADER_START@
 
   6  * This file contains Original Code and/or Modifications of Original Code
 
   7  * as defined in and that are subject to the Apple Public Source License
 
   8  * Version 2.0 (the 'License'). You may not use this file except in
 
   9  * compliance with the License. Please obtain a copy of the License at
 
  10  * http://www.opensource.apple.com/apsl/ and read it before using this
 
  13  * The Original Code and all software distributed under the License are
 
  14  * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 
  15  * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 
  16  * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 
  17  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 
  18  * Please see the License for the specific language governing rights and
 
  19  * limitations under the License.
 
  21  * @APPLE_LICENSE_HEADER_END@
 
  25 #import "SFSQLiteStatement.h"
 
  27 #include <CommonCrypto/CommonDigest.h>
 
  30 #define kSFSQLiteBusyTimeout       (5*60*1000)
 
  32 // Vaccuum our databases approximately once a week
 
  33 #define kCKSQLVacuumInterval       ((60*60*24)*7)
 
  34 #define kSFSQLiteLastVacuumKey     @"LastVacuum"
 
  36 #define kSFSQLiteSchemaVersionKey  @"SchemaVersion"
 
  37 #define kSFSQLiteCreatedDateKey    @"Created"
 
  39 static NSString *const kSFSQLiteCreatePropertiesTableSQL =
 
  40     @"create table if not exists Properties (\n"
 
  41     @"    key    text primary key,\n"
 
  46 NSArray *SFSQLiteJournalSuffixes() {
 
  47     return @[@"-journal", @"-wal", @"-shm"];
 
  50 @interface NSObject (SFSQLiteAdditions)
 
  51 + (NSString *)SFSQLiteClassName;
 
  54 @implementation NSObject (SFSQLiteAdditions)
 
  55 + (NSString *)SFSQLiteClassName {
 
  56     return NSStringFromClass(self);
 
  60 @interface SFSQLite ()
 
  62 @property (nonatomic, assign)            sqlite3                *db;
 
  63 @property (nonatomic, assign)            NSUInteger              openCount;
 
  64 @property (nonatomic, assign)            BOOL                    corrupt;
 
  65 @property (nonatomic, readonly, strong)  NSMutableDictionary    *statementsBySQL;
 
  66 @property (nonatomic, strong)            NSDateFormatter        *dateFormatter;
 
  70 static char intToHexChar(uint8_t i)
 
  72     return i >= 10 ? 'a' + i - 10 : '0' + i;
 
  75 static char *SecHexCharFromBytes(const uint8_t *bytes, NSUInteger length, NSUInteger *outlen) {
 
  76     // Fudge the math a bit on the assert because we don't want a 1GB string anyway
 
  77     if (length > (NSUIntegerMax / 3)) {
 
  80     char *hex = calloc(1, length * 2 * 9 / 8); // 9/8 so we can inline ' ' between every 8 character sequence
 
  85     for (i = 0; length > 4; i += 4, length -= 4) {
 
  86         for (NSUInteger offset = 0; offset < 4; offset++) {
 
  87             *destPtr++ = intToHexChar((bytes[i+offset] & 0xF0) >> 4);
 
  88             *destPtr++ = intToHexChar(bytes[i+offset] & 0x0F);
 
  93     /* Using the same i from the above loop */
 
  94     for (; length > 0; i++, length--) {
 
  95         *destPtr++ = intToHexChar((bytes[i] & 0xF0) >> 4);
 
  96         *destPtr++ = intToHexChar(bytes[i] & 0x0F);
 
  99     if (outlen) *outlen = destPtr - hex;
 
 104 static BOOL SecCreateDirectoryAtPath(NSString *path, NSError **error) {
 
 107     NSFileManager *fileManager = [NSFileManager defaultManager];
 
 109     if (![fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&localError]) {
 
 110         if (![localError.domain isEqualToString:NSCocoaErrorDomain] || localError.code != NSFileWriteFileExistsError) {
 
 117         NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:&localError];
 
 118         if (![attributes[NSFileProtectionKey] isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication]) {
 
 119             [fileManager setAttributes:@{ NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication }
 
 120                           ofItemAtPath:path error:nil];
 
 125         if (error) *error = localError;
 
 130 @implementation NSData (CKUtilsAdditions)
 
 132 - (NSString *)CKHexString {
 
 133     NSUInteger hexLen = 0;
 
 134     NS_VALID_UNTIL_END_OF_SCOPE NSData *arcSafeSelf = self;
 
 135     char *hex = SecHexCharFromBytes([arcSafeSelf bytes], [arcSafeSelf length], &hexLen);
 
 136     return [[NSString alloc] initWithBytesNoCopy:hex length:hexLen encoding:NSASCIIStringEncoding freeWhenDone:YES];
 
 139 - (NSString *)CKLowercaseHexStringWithoutSpaces {
 
 140     NSMutableString *retVal = [[self CKHexString] mutableCopy];
 
 141     [retVal replaceOccurrencesOfString:@" " withString:@"" options:0 range:NSMakeRange(0, [retVal length])];
 
 145 - (NSString *)CKUppercaseHexStringWithoutSpaces {
 
 146     NSMutableString *retVal = [[[self CKHexString] uppercaseString] mutableCopy];
 
 147     [retVal replaceOccurrencesOfString:@" " withString:@"" options:0 range:NSMakeRange(0, [retVal length])];
 
 151 + (NSData *)CKDataWithHexString:(NSString *)hexString stringIsUppercase:(BOOL)stringIsUppercase {
 
 152     NSMutableData *retVal = [[NSMutableData alloc] init];
 
 153     NSCharacterSet *hexCharacterSet = nil;
 
 155     if (stringIsUppercase) {
 
 156         hexCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"];
 
 159         hexCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdef"];
 
 164     for (i = 0; i < [hexString length] ; ) {
 
 165         BOOL validFirstByte = NO;
 
 166         BOOL validSecondByte = NO;
 
 167         unichar firstByte = 0;
 
 168         unichar secondByte = 0;
 
 170         for ( ; i < [hexString length]; i++) {
 
 171             firstByte = [hexString characterAtIndex:i];
 
 172             if ([hexCharacterSet characterIsMember:firstByte]) {
 
 174                 validFirstByte = YES;
 
 178         for ( ; i < [hexString length]; i++) {
 
 179             secondByte = [hexString characterAtIndex:i];
 
 180             if ([hexCharacterSet characterIsMember:secondByte]) {
 
 182                 validSecondByte = YES;
 
 186         if (!validFirstByte || !validSecondByte) {
 
 189         if ((firstByte >= '0') && (firstByte <= '9')) {
 
 192             firstByte = firstByte - aChar + 10;
 
 194         if ((secondByte >= '0') && (secondByte <= '9')) {
 
 197             secondByte = secondByte - aChar + 10;
 
 199         char totalByteValue = (char)((firstByte << 4) + secondByte);
 
 201         [retVal appendBytes:&totalByteValue length:1];
 
 207 + (NSData *)CKDataWithHexString:(NSString *)hexString {
 
 208     return [self CKDataWithHexString:hexString stringIsUppercase:NO];
 
 213 @implementation SFSQLite
 
 215 @synthesize delegate = _delegate;
 
 216 @synthesize path = _path;
 
 217 @synthesize schema = _schema;
 
 218 @synthesize schemaVersion = _schemaVersion;
 
 219 @synthesize objectClassPrefix = _objectClassPrefix;
 
 220 @synthesize userVersion = _userVersion;
 
 221 @synthesize synchronousMode = _synchronousMode;
 
 222 @synthesize hasMigrated = _hasMigrated;
 
 223 @synthesize shouldVacuum = _shouldVacuum;
 
 224 @synthesize traced = _traced;
 
 225 @synthesize db = _db;
 
 226 @synthesize openCount = _openCount;
 
 227 @synthesize corrupt = _corrupt;
 
 228 @synthesize statementsBySQL = _statementsBySQL;
 
 229 @synthesize dateFormatter = _dateFormatter;
 
 231 @synthesize unitTestOverrides = _unitTestOverrides;
 
 234 - (instancetype)initWithPath:(NSString *)path schema:(NSString *)schema {
 
 235     if ((self = [super init])) {
 
 236         NSAssert([path length], @"Can't init a database with a zero-length path");
 
 239         _schemaVersion = [self _createSchemaHash];
 
 240         _statementsBySQL = [[NSMutableDictionary alloc] init];
 
 241         _objectClassPrefix = @"CK";
 
 242         _synchronousMode = SFSQLiteSynchronousModeNormal;
 
 255 - (SInt32)userVersion {
 
 257         return self.delegate.userVersion;
 
 262 - (NSString *)_synchronousModeString {
 
 263     switch (self.synchronousMode) {
 
 264         case SFSQLiteSynchronousModeOff:
 
 266         case SFSQLiteSynchronousModeFull:
 
 268         case SFSQLiteSynchronousModeNormal:
 
 271             assert(0 && "Unknown synchronous mode");
 
 276 - (NSString *)_createSchemaHash {
 
 277     unsigned char hashBuffer[CC_SHA256_DIGEST_LENGTH] = {0};
 
 278     NSData *hashData = [NSData dataWithBytesNoCopy:hashBuffer length:CC_SHA256_DIGEST_LENGTH freeWhenDone:NO];
 
 279     NS_VALID_UNTIL_END_OF_SCOPE NSData *schemaData = [self.schema dataUsingEncoding:NSUTF8StringEncoding];
 
 280     CC_SHA256([schemaData bytes], (CC_LONG)[schemaData length], hashBuffer);
 
 281     return [hashData CKUppercaseHexStringWithoutSpaces];
 
 288 - (void)_periodicVacuum {
 
 289     // "When the auto-vacuum mode is 1 or "full", the freelist pages are moved to the end of the database file and the database file is truncated to remove the freelist pages at every transaction commit.
 
 290     // Note, however, that auto-vacuum only truncates the freelist pages from the file. Auto-vacuum does not defragment the database nor repack individual database pages the way that the VACUUM command does.
 
 291     // In fact, because it moves pages around within the file, auto-vacuum can actually make fragmentation worse."
 
 292     // https://sqlite.org/pragma.html#pragma_auto_vacuum
 
 293     NSDate *lastVacuumDate = [NSDate dateWithTimeIntervalSinceReferenceDate:[[self propertyForKey:kSFSQLiteLastVacuumKey] floatValue]];
 
 294     if ([lastVacuumDate timeIntervalSinceNow] < -(kCKSQLVacuumInterval)) {
 
 295         [self executeSQL:@"VACUUM"];
 
 297         NSString *vacuumDateString = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSinceReferenceDate]];
 
 298         [self setProperty:vacuumDateString forKey:kSFSQLiteLastVacuumKey];
 
 302 - (BOOL)openWithError:(NSError **)error {
 
 305     NSString *dbSchemaVersion, *dir;
 
 307     NS_VALID_UNTIL_END_OF_SCOPE NSString *arcSafePath = _path;
 
 309     if (_openCount > 0) {
 
 310         NSAssert(_db != NULL, @"Missing handle for open cache db");
 
 316     // Create the directory for the cache.
 
 317     dir = [_path stringByDeletingLastPathComponent];
 
 318     if (!SecCreateDirectoryAtPath(dir, &localError)) {
 
 322     int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
 
 324     flags |= SQLITE_OPEN_FILEPROTECTION_COMPLETEUNTILFIRSTUSERAUTHENTICATION;
 
 326     int rc = sqlite3_open_v2([arcSafePath fileSystemRepresentation], &_db, flags, NULL);
 
 327     if (rc != SQLITE_OK) {
 
 328         localError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening db at %@, rc=%d(0x%x)", _path, rc, rc]}];
 
 331     sqlite3_extended_result_codes(_db, 1);
 
 332     rc = sqlite3_busy_timeout(_db, kSFSQLiteBusyTimeout);
 
 333     if (rc != SQLITE_OK) {
 
 337     // You don't argue with the Ben: rdar://12685305
 
 338     if (![self executeSQL:@"pragma journal_mode = WAL"]) {
 
 341     if (![self executeSQL:@"pragma synchronous = %@", [self _synchronousModeString]]) {
 
 344     if (![self executeSQL:@"pragma auto_vacuum = FULL"]) {
 
 348     // rdar://problem/32168789
 
 349     // [self executeSQL:@"pragma foreign_keys = 1"];
 
 351     // Initialize the db within a transaction in case there is a crash between creating the schema and setting the
 
 352     // schema version, and to avoid multiple threads trying to re-create the db at once.
 
 355     // Create the Properties table before trying to read the schema version from it. If the Properties table doesn't
 
 356     // exist we can't prepare a statement to access it.
 
 357     results = [self select:@[@"name"] from:@"sqlite_master" where:@"type = ? AND name = ?" bindings:@[@"table", @"Properties"]];
 
 358     if (!results.count) {
 
 359         [self executeSQL:kSFSQLiteCreatePropertiesTableSQL];
 
 362     // Check the schema version and create or re-create the db if needed.
 
 364     dbSchemaVersion = [self propertyForKey:kSFSQLiteSchemaVersionKey];
 
 365     SInt32 dbUserVersion = [self dbUserVersion];
 
 367     if (!dbSchemaVersion) {
 
 368         // The schema version isn't set so the db was just created or we failed to initialize it previously.
 
 370     } else if (![dbSchemaVersion isEqualToString:self.schemaVersion]
 
 371                || (self.userVersion && dbUserVersion != self.userVersion)) {
 
 373         if (self.delegate && [self.delegate migrateDatabase:self fromVersion:dbUserVersion]) {
 
 378             // The schema version doesn't match and we haven't migrated to the new version. Give up and throw away the db and re-create it instead of trying to migrate.
 
 379             [self removeAllStatements];
 
 380             [self dropAllTables];
 
 386         [self executeSQL:kSFSQLiteCreatePropertiesTableSQL];
 
 387         [self executeSQL:@"%@", self.schema];
 
 388         NSString *createdDateString = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSinceReferenceDate]];
 
 389         [self setProperty:createdDateString forKey:kSFSQLiteCreatedDateKey];
 
 395     // TODO: <rdar://problem/33115830> Resolve Race Condition When Setting 'userVersion/schemaVersion' in SFSQLite
 
 396     if ([self.unitTestOverrides[@"RacyUserVersionUpdate"] isEqual:@YES]) {
 
 402     if (self.shouldVacuum) [self _periodicVacuum];
 
 404     if (create || _hasMigrated) {
 
 405         [self setProperty:self.schemaVersion forKey:kSFSQLiteSchemaVersionKey];
 
 406         if (self.userVersion) {
 
 407             [self executeSQL:@"pragma user_version = %ld", (long)self.userVersion];
 
 416         sqlite3_close_v2(_db);
 
 420     if (!success && error) {
 
 422             localError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening db at %@, ", _path]}];
 
 431     if (![self openWithError:&error]) {
 
 432         secerror("sfsqlite: Error opening db at %@: %@", self.path, error);
 
 439     if (_openCount > 0) {
 
 440         if (_openCount == 1) {
 
 441             NSAssert(_db != NULL, @"Missing handle for open cache db");
 
 443             [self removeAllStatements];
 
 445             if (sqlite3_close(_db)) {
 
 446                 secerror("sfsqlite: Error closing database");
 
 456     NSAssert(_openCount == 0, @"Trying to remove db at: %@ while it is open", _path);
 
 457     [[NSFileManager defaultManager] removeItemAtPath:_path error:nil];
 
 458     for (NSString *suffix in SFSQLiteJournalSuffixes()) {
 
 459         [[NSFileManager defaultManager] removeItemAtPath:[_path stringByAppendingString:suffix] error:nil];
 
 464     [self executeSQL:@"begin exclusive"];
 
 468     [self executeSQL:@"end"];
 
 472     [self executeSQL:@"rollback"];
 
 476     [self executeSQL:@"analyze"];
 
 480     [self executeSQL:@"vacuum"];
 
 483 - (SFSQLiteRowID)lastInsertRowID {
 
 485         secerror("sfsqlite: Database is closed");
 
 489     return sqlite3_last_insert_rowid(_db);
 
 495         secerror("sfsqlite: Database is closed");
 
 499     return sqlite3_changes(_db);
 
 502 - (BOOL)executeSQL:(NSString *)format, ... {
 
 504     va_start(args, format);
 
 505     BOOL result = [self executeSQL:format arguments:args];
 
 510 - (BOOL)executeSQL:(NSString *)format arguments:(va_list)args {
 
 511     NS_VALID_UNTIL_END_OF_SCOPE NSString *SQL = [[NSString alloc] initWithFormat:format arguments:args];
 
 513         secerror("sfsqlite: Database is closed");
 
 516     int execRet = sqlite3_exec(_db, [SQL UTF8String], NULL, NULL, NULL);
 
 517     if (execRet != SQLITE_OK) {
 
 518         secerror("sfsqlite: Error executing SQL: \"%@\" (%d)", SQL, execRet);
 
 525 - (SFSQLiteStatement *)statementForSQL:(NSString *)SQL {
 
 527         secerror("sfsqlite: Database is closed");
 
 531     SFSQLiteStatement *statement = _statementsBySQL[SQL];
 
 533         NSAssert(statement.isReset, @"Statement not reset after last use: \"%@\"", SQL);
 
 535         sqlite3_stmt *handle = NULL;
 
 536         NS_VALID_UNTIL_END_OF_SCOPE NSString *arcSafeSQL = SQL;
 
 537         if (sqlite3_prepare_v2(_db, [arcSafeSQL UTF8String], -1, &handle, NULL)) {
 
 538             secerror("Error preparing statement: %@", SQL);
 
 542         statement = [[SFSQLiteStatement alloc] initWithSQLite:self SQL:SQL handle:handle];
 
 543         _statementsBySQL[SQL] = statement;
 
 549 - (void)removeAllStatements {
 
 550     [[_statementsBySQL allValues] makeObjectsPerformSelector:@selector(finalizeStatement)];
 
 551     [_statementsBySQL removeAllObjects];
 
 554 - (NSArray *)allTableNames {
 
 555     NSMutableArray *tableNames = [[NSMutableArray alloc] init];
 
 557     SFSQLiteStatement *statement = [self statementForSQL:@"select name from sqlite_master where type = 'table'"];
 
 558     while ([statement step]) {
 
 559         NSString *name = [statement textAtIndex:0];
 
 560         [tableNames addObject:name];
 
 567 - (void)dropAllTables {
 
 568     for (NSString *tableName in [self allTableNames]) {
 
 569         [self executeSQL:@"drop table %@", tableName];
 
 573 - (NSString *)propertyForKey:(NSString *)key {
 
 574     NSAssert(key, @"Null key");
 
 576     NSString *value = nil;
 
 578     SFSQLiteStatement *statement = [self statementForSQL:@"select value from Properties where key = ?"];
 
 579     [statement bindText:key atIndex:0];
 
 580     if ([statement step]) {
 
 581         value = [statement textAtIndex:0];
 
 588 - (void)setProperty:(NSString *)value forKey:(NSString *)key {
 
 589     NSAssert(key, @"Null key");
 
 592         SFSQLiteStatement *statement = [self statementForSQL:@"insert or replace into Properties (key, value) values (?,?)"];
 
 593         [statement bindText:key atIndex:0];
 
 594         [statement bindText:value atIndex:1];
 
 598         [self removePropertyForKey:key];
 
 602 - (NSDateFormatter *)dateFormatter {
 
 603     if (!_dateFormatter) {
 
 604         NSDateFormatter* dateFormatter = [NSDateFormatter new];
 
 605         dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZZZZZ";
 
 606         _dateFormatter = dateFormatter;
 
 608     return _dateFormatter;
 
 611 - (NSDate *)datePropertyForKey:(NSString *)key {
 
 612     NSString *dateStr = [self propertyForKey:key];
 
 613     if (dateStr.length) {
 
 614         return [self.dateFormatter dateFromString:dateStr];
 
 619 - (void)setDateProperty:(NSDate *)value forKey:(NSString *)key {
 
 620     NSString *dateStr = nil;
 
 622         dateStr = [self.dateFormatter stringFromDate:value];
 
 624     [self setProperty:dateStr forKey:key];
 
 627 - (void)removePropertyForKey:(NSString *)key {
 
 628     NSAssert(key, @"Null key");
 
 630     SFSQLiteStatement *statement = [self statementForSQL:@"delete from Properties where key = ?"];
 
 631     [statement bindText:key atIndex:0];
 
 636 - (NSDate *)creationDate {
 
 637     return [NSDate dateWithTimeIntervalSinceReferenceDate:[[self propertyForKey:kSFSQLiteCreatedDateKey] floatValue]];
 
 640 // https://sqlite.org/pragma.html#pragma_table_info
 
 641 - (NSSet<NSString*> *)columnNamesForTable:(NSString*)tableName {
 
 642     SFSQLiteStatement *statement = [self statementForSQL:[NSString stringWithFormat:@"pragma table_info(%@)", tableName]];
 
 643     NSMutableSet<NSString*>* columnNames = [[NSMutableSet alloc] init];
 
 644     while ([statement step]) {
 
 645         [columnNames addObject:[statement textAtIndex:1]];
 
 651 - (NSArray *)select:(NSArray *)columns from:(NSString *)tableName {
 
 652     return [self select:columns from:tableName where:nil bindings:nil];
 
 655 - (NSArray *)select:(NSArray *)columns from:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
 
 656     NSMutableArray *results = [[NSMutableArray alloc] init];
 
 658     NSMutableString *SQL = [NSMutableString stringWithFormat:@"select %@ from %@", [columns componentsJoinedByString:@", "], tableName];
 
 660         [SQL appendFormat:@" where %@", whereSQL];
 
 663     SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 664     [statement bindValues:bindings];
 
 665     while ([statement step]) {
 
 666         [results addObject:[statement allObjectsByColumnName]];
 
 673 - (void)select:(NSArray *)columns from:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings orderBy:(NSArray *)orderBy limit:(NSNumber *)limit block:(void (^)(NSDictionary *resultDictionary, BOOL *stop))block {
 
 675         NSMutableString *SQL = [[NSMutableString alloc] init];
 
 676         NSString *columnsString = @"*";
 
 677         if ([columns count]) columnsString = [columns componentsJoinedByString:@", "];
 
 678         [SQL appendFormat:@"select %@ from %@", columnsString, tableName];
 
 680         if (whereSQL.length) {
 
 681             [SQL appendFormat:@" where %@", whereSQL];
 
 684             NSString *orderByString = [orderBy componentsJoinedByString:@", "];
 
 685             [SQL appendFormat:@" order by %@", orderByString];
 
 688             [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
 
 691         SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 692         [statement bindValues:bindings];
 
 695                 if (![statement step]) {
 
 698                 NSDictionary *stepResult = [statement allObjectsByColumnName];
 
 701                     block(stepResult, &stop);
 
 712 - (void)selectFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings orderBy:(NSArray *)orderBy limit:(NSNumber *)limit block:(void (^)(NSDictionary *resultDictionary, BOOL *stop))block {
 
 714         NSMutableString *SQL = [[NSMutableString alloc] init];
 
 715         [SQL appendFormat:@"select * from %@", tableName];
 
 717         if (whereSQL.length) {
 
 718             [SQL appendFormat:@" where %@", whereSQL];
 
 721             NSString *orderByString = [orderBy componentsJoinedByString:@", "];
 
 722             [SQL appendFormat:@" order by %@", orderByString];
 
 725             [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
 
 728         SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 729         [statement bindValues:bindings];
 
 732                 if (![statement step]) {
 
 735                 NSDictionary *stepResult = [statement allObjectsByColumnName];
 
 738                     block(stepResult, &stop);
 
 749 - (NSArray *)selectFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings limit:(NSNumber *)limit {
 
 750     NSMutableString *SQL = [[NSMutableString alloc] init];
 
 751     [SQL appendFormat:@"select * from %@", tableName];
 
 753     if (whereSQL.length) {
 
 754         [SQL appendFormat:@" where %@", whereSQL];
 
 757         [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
 
 760     NSMutableArray *results = [[NSMutableArray alloc] init];
 
 762     SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 763     [statement bindValues:bindings];
 
 764     while ([statement step]) {
 
 765         [results addObject:[statement allObjectsByColumnName]];
 
 772 - (void)update:(NSString *)tableName set:(NSString *)setSQL where:(NSString *)whereSQL bindings:(NSArray *)whereBindings limit:(NSNumber *)limit {
 
 773     NSMutableString *SQL = [[NSMutableString alloc] init];
 
 774     [SQL appendFormat:@"update %@", tableName];
 
 776     NSAssert(setSQL.length > 0, @"null set expression");
 
 778     [SQL appendFormat:@" set %@", setSQL];
 
 779     if (whereSQL.length) {
 
 780         [SQL appendFormat:@" where %@", whereSQL];
 
 783         [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
 
 786     SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 787     [statement bindValues:whereBindings];
 
 788     while ([statement step]) {
 
 793 - (NSArray *)selectAllFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
 
 794     return [self selectFrom:tableName where:whereSQL bindings:bindings limit:nil];
 
 797 - (NSUInteger)selectCountFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
 
 798     NSArray *results = [self select:@[@"count(*) as n"] from:tableName where:whereSQL bindings:bindings];
 
 799     return [results[0][@"n"] unsignedIntegerValue];
 
 802 - (SFSQLiteRowID)insertOrReplaceInto:(NSString *)tableName values:(NSDictionary *)valuesByColumnName {
 
 803     NSArray *columnNames = [[valuesByColumnName allKeys] sortedArrayUsingSelector:@selector(compare:)];
 
 804     NSMutableArray *values = [[NSMutableArray alloc] init];
 
 805     for (NSUInteger i = 0; i < columnNames.count; i++) {
 
 806         values[i] = valuesByColumnName[columnNames[i]];
 
 809     NSMutableString *SQL = [[NSMutableString alloc] initWithString:@"insert or replace into "];
 
 810     [SQL appendString:tableName];
 
 811     [SQL appendString:@" ("];
 
 812     for (NSUInteger i = 0; i < columnNames.count; i++) {
 
 813         [SQL appendString:columnNames[i]];
 
 814         if (i != columnNames.count-1) {
 
 815             [SQL appendString:@","];
 
 818     [SQL appendString:@") values ("];
 
 819     for (NSUInteger i = 0; i < columnNames.count; i++) {
 
 820         if (i != columnNames.count-1) {
 
 821             [SQL appendString:@"?,"];
 
 823             [SQL appendString:@"?"];
 
 826     [SQL appendString:@")"];
 
 828     SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 829     [statement bindValues:values];
 
 833     return [self lastInsertRowID];
 
 836 - (void)deleteFrom:(NSString *)tableName matchingValues:(NSDictionary *)valuesByColumnName {
 
 837     NSArray *columnNames = [[valuesByColumnName allKeys] sortedArrayUsingSelector:@selector(compare:)];
 
 838     NSMutableArray *values = [[NSMutableArray alloc] init];
 
 839     NSMutableString *whereSQL = [[NSMutableString alloc] init];
 
 840     int bindingCount = 0;
 
 841     for (NSUInteger i = 0; i < columnNames.count; i++) {
 
 842         id value = valuesByColumnName[columnNames[i]];
 
 843         [whereSQL appendString:columnNames[i]];
 
 844         if (!value || [[NSNull null] isEqual:value]) {
 
 845             [whereSQL appendString:@" is NULL"];
 
 847             values[bindingCount++] = value;
 
 848             [whereSQL appendString:@"=?"];
 
 850         if (i != columnNames.count-1) {
 
 851             [whereSQL appendString:@" AND "];
 
 854     [self deleteFrom:tableName where:whereSQL bindings:values];
 
 857 - (void)deleteFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
 
 858     NSString *SQL = [NSString stringWithFormat:@"delete from %@ where %@", tableName, whereSQL];
 
 860     SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 861     [statement bindValues:bindings];
 
 866 - (NSString *)_tableNameForClass:(Class)objectClass {
 
 867     NSString *className = [objectClass SFSQLiteClassName];
 
 868     if (![className hasPrefix:_objectClassPrefix]) {
 
 869         secerror("sfsqlite: %@", [NSString stringWithFormat:@"Object class \"%@\" does not have prefix \"%@\"", className, _objectClassPrefix]);
 
 872     return [className substringFromIndex:_objectClassPrefix.length];
 
 875 - (SInt32)dbUserVersion {
 
 876     SInt32 userVersion = 0;
 
 877     SFSQLiteStatement *statement = [self statementForSQL:@"pragma user_version"];
 
 878     while ([statement step]) {
 
 879         userVersion = [statement intAtIndex:0];