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@
 
  27 #import "SFSQLiteStatement.h"
 
  29 #include <CommonCrypto/CommonDigest.h>
 
  30 #import "utilities/debugging.h"
 
  31 #import "utilities/simulatecrash_assert.h"
 
  32 #include <os/transaction_private.h>
 
  34 #define kSFSQLiteBusyTimeout       (5*60*1000)
 
  36 #define kSFSQLiteSchemaVersionKey  @"SchemaVersion"
 
  37 #define kSFSQLiteCreatedDateKey    @"Created"
 
  38 #define kSFSQLiteAutoVacuumFull    1
 
  40 static NSString *const kSFSQLiteCreatePropertiesTableSQL =
 
  41     @"create table if not exists Properties (\n"
 
  42     @"    key    text primary key,\n"
 
  47 NSArray *SFSQLiteJournalSuffixes() {
 
  48     return @[@"-journal", @"-wal", @"-shm"];
 
  51 @interface NSObject (SFSQLiteAdditions)
 
  52 + (NSString *)SFSQLiteClassName;
 
  55 @implementation NSObject (SFSQLiteAdditions)
 
  56 + (NSString *)SFSQLiteClassName {
 
  57     return NSStringFromClass(self);
 
  61 @interface SFSQLite ()
 
  63 @property (nonatomic, assign)            sqlite3                *db;
 
  64 @property (nonatomic, assign)            NSUInteger              openCount;
 
  65 @property (nonatomic, assign)            BOOL                    corrupt;
 
  66 @property (nonatomic, readonly, strong)  NSMutableDictionary    *statementsBySQL;
 
  67 @property (nonatomic, strong)            NSDateFormatter        *dateFormatter;
 
  68 @property (nonatomic, strong)            NSDateFormatter        *oldDateFormatter;
 
  72 static char intToHexChar(uint8_t i)
 
  74     return i >= 10 ? 'a' + i - 10 : '0' + i;
 
  77 static char *SecHexCharFromBytes(const uint8_t *bytes, NSUInteger length, NSUInteger *outlen) {
 
  78     // Fudge the math a bit on the assert because we don't want a 1GB string anyway
 
  79     if (length > (NSUIntegerMax / 3)) {
 
  82     char *hex = calloc(1, length * 2 * 9 / 8); // 9/8 so we can inline ' ' between every 8 character sequence
 
  87     for (i = 0; length > 4; i += 4, length -= 4) {
 
  88         for (NSUInteger offset = 0; offset < 4; offset++) {
 
  89             *destPtr++ = intToHexChar((bytes[i+offset] & 0xF0) >> 4);
 
  90             *destPtr++ = intToHexChar(bytes[i+offset] & 0x0F);
 
  95     /* Using the same i from the above loop */
 
  96     for (; length > 0; i++, length--) {
 
  97         *destPtr++ = intToHexChar((bytes[i] & 0xF0) >> 4);
 
  98         *destPtr++ = intToHexChar(bytes[i] & 0x0F);
 
 101     if (outlen) *outlen = destPtr - hex;
 
 106 static BOOL SecCreateDirectoryAtPath(NSString *path, NSError **error) {
 
 109     NSFileManager *fileManager = [NSFileManager defaultManager];
 
 111     if (![fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&localError]) {
 
 112         if (![localError.domain isEqualToString:NSCocoaErrorDomain] || localError.code != NSFileWriteFileExistsError) {
 
 119         NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:&localError];
 
 120         if (![attributes[NSFileProtectionKey] isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication]) {
 
 121             [fileManager setAttributes:@{ NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication }
 
 122                           ofItemAtPath:path error:nil];
 
 127         if (error) *error = localError;
 
 132 @implementation NSData (CKUtilsAdditions)
 
 134 - (NSString *)CKHexString {
 
 135     NSUInteger hexLen = 0;
 
 136     NS_VALID_UNTIL_END_OF_SCOPE NSData *arcSafeSelf = self;
 
 137     char *hex = SecHexCharFromBytes([arcSafeSelf bytes], [arcSafeSelf length], &hexLen);
 
 138     return [[NSString alloc] initWithBytesNoCopy:hex length:hexLen encoding:NSASCIIStringEncoding freeWhenDone:YES];
 
 141 - (NSString *)CKLowercaseHexStringWithoutSpaces {
 
 142     NSMutableString *retVal = [[self CKHexString] mutableCopy];
 
 143     [retVal replaceOccurrencesOfString:@" " withString:@"" options:0 range:NSMakeRange(0, [retVal length])];
 
 147 - (NSString *)CKUppercaseHexStringWithoutSpaces {
 
 148     NSMutableString *retVal = [[[self CKHexString] uppercaseString] mutableCopy];
 
 149     [retVal replaceOccurrencesOfString:@" " withString:@"" options:0 range:NSMakeRange(0, [retVal length])];
 
 153 + (NSData *)CKDataWithHexString:(NSString *)hexString stringIsUppercase:(BOOL)stringIsUppercase {
 
 154     NSMutableData *retVal = [[NSMutableData alloc] init];
 
 155     NSCharacterSet *hexCharacterSet = nil;
 
 157     if (stringIsUppercase) {
 
 158         hexCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"];
 
 161         hexCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdef"];
 
 166     for (i = 0; i < [hexString length] ; ) {
 
 167         BOOL validFirstByte = NO;
 
 168         BOOL validSecondByte = NO;
 
 169         unichar firstByte = 0;
 
 170         unichar secondByte = 0;
 
 172         for ( ; i < [hexString length]; i++) {
 
 173             firstByte = [hexString characterAtIndex:i];
 
 174             if ([hexCharacterSet characterIsMember:firstByte]) {
 
 176                 validFirstByte = YES;
 
 180         for ( ; i < [hexString length]; i++) {
 
 181             secondByte = [hexString characterAtIndex:i];
 
 182             if ([hexCharacterSet characterIsMember:secondByte]) {
 
 184                 validSecondByte = YES;
 
 188         if (!validFirstByte || !validSecondByte) {
 
 191         if ((firstByte >= '0') && (firstByte <= '9')) {
 
 194             firstByte = firstByte - aChar + 10;
 
 196         if ((secondByte >= '0') && (secondByte <= '9')) {
 
 199             secondByte = secondByte - aChar + 10;
 
 201         char totalByteValue = (char)((firstByte << 4) + secondByte);
 
 203         [retVal appendBytes:&totalByteValue length:1];
 
 209 + (NSData *)CKDataWithHexString:(NSString *)hexString {
 
 210     return [self CKDataWithHexString:hexString stringIsUppercase:NO];
 
 215 @implementation SFSQLite
 
 217 @synthesize delegate = _delegate;
 
 218 @synthesize path = _path;
 
 219 @synthesize schema = _schema;
 
 220 @synthesize schemaVersion = _schemaVersion;
 
 221 @synthesize objectClassPrefix = _objectClassPrefix;
 
 222 @synthesize userVersion = _userVersion;
 
 223 @synthesize synchronousMode = _synchronousMode;
 
 224 @synthesize hasMigrated = _hasMigrated;
 
 225 @synthesize traced = _traced;
 
 226 @synthesize db = _db;
 
 227 @synthesize openCount = _openCount;
 
 228 @synthesize corrupt = _corrupt;
 
 229 @synthesize statementsBySQL = _statementsBySQL;
 
 230 @synthesize dateFormatter = _dateFormatter;
 
 231 @synthesize oldDateFormatter = _oldDateFormatter;
 
 233 @synthesize unitTestOverrides = _unitTestOverrides;
 
 236 - (instancetype)initWithPath:(NSString *)path schema:(NSString *)schema {
 
 237     if (![path length]) {
 
 238         seccritical("Cannot init db with empty path");
 
 241     if (![schema length]) {
 
 242         seccritical("Cannot init db without schema");
 
 246     if ((self = [super init])) {
 
 249         _schemaVersion = [self _createSchemaHash];
 
 250         _statementsBySQL = [[NSMutableDictionary alloc] init];
 
 251         _objectClassPrefix = @"CK";
 
 252         _synchronousMode = SFSQLiteSynchronousModeNormal;
 
 264 - (SInt32)userVersion {
 
 266         return self.delegate.userVersion;
 
 271 - (NSString *)_synchronousModeString {
 
 272     switch (self.synchronousMode) {
 
 273         case SFSQLiteSynchronousModeOff:
 
 275         case SFSQLiteSynchronousModeFull:
 
 277         case SFSQLiteSynchronousModeNormal:
 
 280             assert(0 && "Unknown synchronous mode");
 
 285 - (NSString *)_createSchemaHash {
 
 286     unsigned char hashBuffer[CC_SHA256_DIGEST_LENGTH] = {0};
 
 287     NSData *hashData = [NSData dataWithBytesNoCopy:hashBuffer length:CC_SHA256_DIGEST_LENGTH freeWhenDone:NO];
 
 288     NS_VALID_UNTIL_END_OF_SCOPE NSData *schemaData = [self.schema dataUsingEncoding:NSUTF8StringEncoding];
 
 289     CC_SHA256([schemaData bytes], (CC_LONG)[schemaData length], hashBuffer);
 
 290     return [hashData CKUppercaseHexStringWithoutSpaces];
 
 298  Best-effort attempts to set/correct filesystem permissions.
 
 299  May fail when we don't own DB which means we must wait for them to update permissions,
 
 300  or file does not exist yet which is okay because db will exist and the aux files inherit permissions
 
 302 - (void)attemptProperDatabasePermissions
 
 304     NSFileManager* fm = [NSFileManager defaultManager];
 
 305     [fm setAttributes:@{NSFilePosixPermissions : [NSNumber numberWithShort:0666]}
 
 308     [fm setAttributes:@{NSFilePosixPermissions : [NSNumber numberWithShort:0666]}
 
 309          ofItemAtPath:[NSString stringWithFormat:@"%@-wal",_path]
 
 311     [fm setAttributes:@{NSFilePosixPermissions : [NSNumber numberWithShort:0666]}
 
 312          ofItemAtPath:[NSString stringWithFormat:@"%@-shm",_path]
 
 316 - (BOOL)openWithError:(NSError **)error {
 
 319     NSString *dbSchemaVersion, *dir;
 
 321     NS_VALID_UNTIL_END_OF_SCOPE NSString *arcSafePath = _path;
 
 323     if (_openCount > 0) {
 
 324         NSAssert(_db != NULL, @"Missing handle for open cache db");
 
 330     // Create the directory for the cache.
 
 331     dir = [_path stringByDeletingLastPathComponent];
 
 332     if (!SecCreateDirectoryAtPath(dir, &localError)) {
 
 336     int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
 
 338     flags |= SQLITE_OPEN_FILEPROTECTION_COMPLETEUNTILFIRSTUSERAUTHENTICATION;
 
 340     int rc = sqlite3_open_v2([arcSafePath fileSystemRepresentation], &_db, flags, NULL);
 
 341     if (rc != SQLITE_OK) {
 
 342         localError = [NSError errorWithDomain:NSCocoaErrorDomain code:rc userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening db at %@, rc=%d(0x%x)", _path, rc, rc]}];
 
 346     // Filesystem foo for multiple daemons from different users
 
 347     [self attemptProperDatabasePermissions];
 
 349     sqlite3_extended_result_codes(_db, 1);
 
 350     rc = sqlite3_busy_timeout(_db, kSFSQLiteBusyTimeout);
 
 351     if (rc != SQLITE_OK) {
 
 355     // You don't argue with the Ben: rdar://12685305
 
 356     if (![self executeSQL:@"pragma journal_mode = WAL"]) {
 
 359     if (![self executeSQL:@"pragma synchronous = %@", [self _synchronousModeString]]) {
 
 362     if ([self autoVacuumSetting] != kSFSQLiteAutoVacuumFull) {
 
 363         /* After changing the auto_vacuum setting the DB must be vacuumed */
 
 364         if (![self executeSQL:@"pragma auto_vacuum = FULL"] || ![self executeSQL:@"VACUUM"]) {
 
 369     // rdar://problem/32168789
 
 370     // [self executeSQL:@"pragma foreign_keys = 1"];
 
 372     // Initialize the db within a transaction in case there is a crash between creating the schema and setting the
 
 373     // schema version, and to avoid multiple threads trying to re-create the db at once.
 
 376     // Create the Properties table before trying to read the schema version from it. If the Properties table doesn't
 
 377     // exist we can't prepare a statement to access it.
 
 378     results = [self select:@[@"name"] from:@"sqlite_master" where:@"type = ? AND name = ?" bindings:@[@"table", @"Properties"]];
 
 379     if (!results.count) {
 
 380         [self executeSQL:kSFSQLiteCreatePropertiesTableSQL];
 
 383     // Check the schema version and create or re-create the db if needed.
 
 385     dbSchemaVersion = [self propertyForKey:kSFSQLiteSchemaVersionKey];
 
 386     SInt32 dbUserVersion = [self dbUserVersion];
 
 388     if (!dbSchemaVersion) {
 
 389         // The schema version isn't set so the db was just created or we failed to initialize it previously.
 
 391     } else if (![dbSchemaVersion isEqualToString:self.schemaVersion]
 
 392                || (self.userVersion && dbUserVersion != self.userVersion)) {
 
 394         if (self.delegate && [self.delegate migrateDatabase:self fromVersion:dbUserVersion]) {
 
 399             // 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.
 
 400             [self removeAllStatements];
 
 401             [self dropAllTables];
 
 407         [self executeSQL:kSFSQLiteCreatePropertiesTableSQL];
 
 408         [self executeSQL:@"%@", self.schema];
 
 409         NSString *createdDateString = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSinceReferenceDate]];
 
 410         [self setProperty:createdDateString forKey:kSFSQLiteCreatedDateKey];
 
 416     // TODO: <rdar://problem/33115830> Resolve Race Condition When Setting 'userVersion/schemaVersion' in SFSQLite
 
 417     if ([self.unitTestOverrides[@"RacyUserVersionUpdate"] isEqual:@YES]) {
 
 423     if (create || _hasMigrated) {
 
 424         [self setProperty:self.schemaVersion forKey:kSFSQLiteSchemaVersionKey];
 
 425         if (self.userVersion) {
 
 426             [self executeSQL:@"pragma user_version = %ld", (long)self.userVersion];
 
 435         sqlite3_close_v2(_db);
 
 439     if (!success && error) {
 
 441             localError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening db at %@", _path]}];
 
 450     if (![self openWithError:&error] && !(error && error.code == SQLITE_AUTH)) {
 
 451         secerror("sfsqlite: Error opening db at %@: %@", self.path, error);
 
 458     if (_openCount > 0) {
 
 459         if (_openCount == 1) {
 
 460             NSAssert(_db != NULL, @"Missing handle for open cache db");
 
 462             [self removeAllStatements];
 
 464             if (sqlite3_close(_db)) {
 
 465                 secerror("sfsqlite: Error closing database");
 
 475     NSAssert(_openCount == 0, @"Trying to remove db at: %@ while it is open", _path);
 
 476     [[NSFileManager defaultManager] removeItemAtPath:_path error:nil];
 
 477     for (NSString *suffix in SFSQLiteJournalSuffixes()) {
 
 478         [[NSFileManager defaultManager] removeItemAtPath:[_path stringByAppendingString:suffix] error:nil];
 
 483     [self executeSQL:@"begin exclusive"];
 
 487     [self executeSQL:@"end"];
 
 491     [self executeSQL:@"rollback"];
 
 495     [self executeSQL:@"analyze"];
 
 499     [self executeSQL:@"vacuum"];
 
 502 - (SFSQLiteRowID)lastInsertRowID {
 
 504         secerror("sfsqlite: Database is closed");
 
 508     return sqlite3_last_insert_rowid(_db);
 
 514         secerror("sfsqlite: Database is closed");
 
 518     return sqlite3_changes(_db);
 
 521 - (BOOL)executeSQL:(NSString *)format, ... {
 
 523     va_start(args, format);
 
 524     BOOL result = [self executeSQL:format arguments:args];
 
 529 - (BOOL)executeSQL:(NSString *)format arguments:(va_list)args {
 
 530     NS_VALID_UNTIL_END_OF_SCOPE NSString *SQL = [[NSString alloc] initWithFormat:format arguments:args];
 
 532         secerror("sfsqlite: Database is closed");
 
 535     int execRet = sqlite3_exec(_db, [SQL UTF8String], NULL, NULL, NULL);
 
 536     if (execRet != SQLITE_OK) {
 
 537         if (execRet != SQLITE_AUTH && execRet != SQLITE_READONLY) {
 
 538             secerror("sfsqlite: Error executing SQL: \"%@\" (%d)", SQL, execRet);
 
 546 - (SFSQLiteStatement *)statementForSQL:(NSString *)SQL {
 
 548         secerror("sfsqlite: Database is closed");
 
 552     SFSQLiteStatement *statement = _statementsBySQL[SQL];
 
 554         NSAssert(statement.isReset, @"Statement not reset after last use: \"%@\"", SQL);
 
 556         sqlite3_stmt *handle = NULL;
 
 557         NS_VALID_UNTIL_END_OF_SCOPE NSString *arcSafeSQL = SQL;
 
 558         if (sqlite3_prepare_v2(_db, [arcSafeSQL UTF8String], -1, &handle, NULL)) {
 
 559             secerror("Error preparing statement: %@", SQL);
 
 563         statement = [[SFSQLiteStatement alloc] initWithSQLite:self SQL:SQL handle:handle];
 
 564         _statementsBySQL[SQL] = statement;
 
 570 - (void)removeAllStatements {
 
 571     [[_statementsBySQL allValues] makeObjectsPerformSelector:@selector(finalizeStatement)];
 
 572     [_statementsBySQL removeAllObjects];
 
 575 - (NSArray *)allTableNames {
 
 576     NSMutableArray *tableNames = [[NSMutableArray alloc] init];
 
 578     SFSQLiteStatement *statement = [self statementForSQL:@"select name from sqlite_master where type = 'table'"];
 
 579     while ([statement step]) {
 
 580         NSString *name = [statement textAtIndex:0];
 
 581         [tableNames addObject:name];
 
 588 - (void)dropAllTables {
 
 589     for (NSString *tableName in [self allTableNames]) {
 
 590         [self executeSQL:@"drop table %@", tableName];
 
 594 - (NSString *)propertyForKey:(NSString *)key {
 
 596         secerror("SFSQLite: attempt to retrieve property without a key");
 
 600     NSString *value = nil;
 
 602     SFSQLiteStatement *statement = [self statementForSQL:@"select value from Properties where key = ?"];
 
 603     [statement bindText:key atIndex:0];
 
 604     if ([statement step]) {
 
 605         value = [statement textAtIndex:0];
 
 612 - (void)setProperty:(NSString *)value forKey:(NSString *)key {
 
 614         secerror("SFSQLite: attempt to set property without a key");
 
 619         SFSQLiteStatement *statement = [self statementForSQL:@"insert or replace into Properties (key, value) values (?,?)"];
 
 620         [statement bindText:key atIndex:0];
 
 621         [statement bindText:value atIndex:1];
 
 625         [self removePropertyForKey:key];
 
 629 - (NSDateFormatter *)dateFormatter {
 
 630     if (!_dateFormatter) {
 
 631         NSDateFormatter* dateFormatter = [NSDateFormatter new];
 
 632         dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZ";
 
 633         _dateFormatter = dateFormatter;
 
 635     return _dateFormatter;
 
 638 - (NSDateFormatter *)oldDateFormatter {
 
 639     if (!_oldDateFormatter) {
 
 640         NSDateFormatter* dateFormatter = [NSDateFormatter new];
 
 641         dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZZZZZ";
 
 642         _oldDateFormatter = dateFormatter;
 
 644     return _oldDateFormatter;
 
 647 - (NSDate *)datePropertyForKey:(NSString *)key {
 
 648     NSString *dateStr = [self propertyForKey:key];
 
 649     if (dateStr.length) {
 
 650         NSDate  *date = [self.dateFormatter dateFromString:dateStr];
 
 652             date = [self.oldDateFormatter dateFromString:dateStr];
 
 659 - (void)setDateProperty:(NSDate *)value forKey:(NSString *)key {
 
 660     NSString *dateStr = nil;
 
 662         dateStr = [self.dateFormatter stringFromDate:value];
 
 664     [self setProperty:dateStr forKey:key];
 
 667 - (void)removePropertyForKey:(NSString *)key {
 
 672     SFSQLiteStatement *statement = [self statementForSQL:@"delete from Properties where key = ?"];
 
 673     [statement bindText:key atIndex:0];
 
 678 - (NSDate *)creationDate {
 
 679     return [NSDate dateWithTimeIntervalSinceReferenceDate:[[self propertyForKey:kSFSQLiteCreatedDateKey] floatValue]];
 
 682 // https://sqlite.org/pragma.html#pragma_table_info
 
 683 - (NSSet<NSString*> *)columnNamesForTable:(NSString*)tableName {
 
 684     SFSQLiteStatement *statement = [self statementForSQL:[NSString stringWithFormat:@"pragma table_info(%@)", tableName]];
 
 685     NSMutableSet<NSString*>* columnNames = [[NSMutableSet alloc] init];
 
 686     while ([statement step]) {
 
 687         [columnNames addObject:[statement textAtIndex:1]];
 
 693 - (NSArray *)select:(NSArray *)columns from:(NSString *)tableName {
 
 694     return [self select:columns from:tableName where:nil bindings:nil];
 
 697 - (NSArray *)select:(NSArray *)columns from:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
 
 698     NSMutableArray *results = [[NSMutableArray alloc] init];
 
 700     NSMutableString *SQL = [NSMutableString stringWithFormat:@"select %@ from %@", [columns componentsJoinedByString:@", "], tableName];
 
 702         [SQL appendFormat:@" where %@", whereSQL];
 
 705     SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 706     [statement bindValues:bindings];
 
 707     while ([statement step]) {
 
 708         [results addObject:[statement allObjectsByColumnName]];
 
 715 - (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 {
 
 717         NSMutableString *SQL = [[NSMutableString alloc] init];
 
 718         NSString *columnsString = @"*";
 
 719         if ([columns count]) columnsString = [columns componentsJoinedByString:@", "];
 
 720         [SQL appendFormat:@"select %@ from %@", columnsString, tableName];
 
 722         if (whereSQL.length) {
 
 723             [SQL appendFormat:@" where %@", whereSQL];
 
 726             NSString *orderByString = [orderBy componentsJoinedByString:@", "];
 
 727             [SQL appendFormat:@" order by %@", orderByString];
 
 730             [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
 
 733         SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 734         [statement bindValues:bindings];
 
 737                 if (![statement step]) {
 
 740                 NSDictionary *stepResult = [statement allObjectsByColumnName];
 
 743                     block(stepResult, &stop);
 
 754 - (void)selectFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings orderBy:(NSArray *)orderBy limit:(NSNumber *)limit block:(void (^)(NSDictionary *resultDictionary, BOOL *stop))block {
 
 756         NSMutableString *SQL = [[NSMutableString alloc] init];
 
 757         [SQL appendFormat:@"select * from %@", tableName];
 
 759         if (whereSQL.length) {
 
 760             [SQL appendFormat:@" where %@", whereSQL];
 
 763             NSString *orderByString = [orderBy componentsJoinedByString:@", "];
 
 764             [SQL appendFormat:@" order by %@", orderByString];
 
 767             [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
 
 770         SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 771         [statement bindValues:bindings];
 
 774                 if (![statement step]) {
 
 777                 NSDictionary *stepResult = [statement allObjectsByColumnName];
 
 780                     block(stepResult, &stop);
 
 791 - (NSArray *)selectFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings limit:(NSNumber *)limit {
 
 792     NSMutableString *SQL = [[NSMutableString alloc] init];
 
 793     [SQL appendFormat:@"select * from %@", tableName];
 
 795     if (whereSQL.length) {
 
 796         [SQL appendFormat:@" where %@", whereSQL];
 
 799         [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
 
 802     NSMutableArray *results = [[NSMutableArray alloc] init];
 
 804     SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 805     [statement bindValues:bindings];
 
 806     while ([statement step]) {
 
 807         [results addObject:[statement allObjectsByColumnName]];
 
 814 - (void)update:(NSString *)tableName set:(NSString *)setSQL where:(NSString *)whereSQL bindings:(NSArray *)whereBindings limit:(NSNumber *)limit {
 
 815     if (![setSQL length]) {
 
 819     NSMutableString *SQL = [[NSMutableString alloc] init];
 
 820     [SQL appendFormat:@"update %@", tableName];
 
 822     [SQL appendFormat:@" set %@", setSQL];
 
 823     if (whereSQL.length) {
 
 824         [SQL appendFormat:@" where %@", whereSQL];
 
 827         [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
 
 830     SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 831     [statement bindValues:whereBindings];
 
 832     while ([statement step]) {
 
 837 - (NSArray *)selectAllFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
 
 838     return [self selectFrom:tableName where:whereSQL bindings:bindings limit:nil];
 
 841 - (NSUInteger)selectCountFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
 
 842     NSArray *results = [self select:@[@"count(*) as n"] from:tableName where:whereSQL bindings:bindings];
 
 843     return [results[0][@"n"] unsignedIntegerValue];
 
 846 - (SFSQLiteRowID)insertOrReplaceInto:(NSString *)tableName values:(NSDictionary *)valuesByColumnName {
 
 847     NSArray *columnNames = [[valuesByColumnName allKeys] sortedArrayUsingSelector:@selector(compare:)];
 
 848     NSMutableArray *values = [[NSMutableArray alloc] init];
 
 849     for (NSUInteger i = 0; i < columnNames.count; i++) {
 
 850         values[i] = valuesByColumnName[columnNames[i]];
 
 853     NSMutableString *SQL = [[NSMutableString alloc] initWithString:@"insert or replace into "];
 
 854     [SQL appendString:tableName];
 
 855     [SQL appendString:@" ("];
 
 856     for (NSUInteger i = 0; i < columnNames.count; i++) {
 
 857         [SQL appendString:columnNames[i]];
 
 858         if (i != columnNames.count-1) {
 
 859             [SQL appendString:@","];
 
 862     [SQL appendString:@") values ("];
 
 863     for (NSUInteger i = 0; i < columnNames.count; i++) {
 
 864         if (i != columnNames.count-1) {
 
 865             [SQL appendString:@"?,"];
 
 867             [SQL appendString:@"?"];
 
 870     [SQL appendString:@")"];
 
 872     SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 873     [statement bindValues:values];
 
 877     return [self lastInsertRowID];
 
 880 - (void)deleteFrom:(NSString *)tableName matchingValues:(NSDictionary *)valuesByColumnName {
 
 881     NSArray *columnNames = [[valuesByColumnName allKeys] sortedArrayUsingSelector:@selector(compare:)];
 
 882     NSMutableArray *values = [[NSMutableArray alloc] init];
 
 883     NSMutableString *whereSQL = [[NSMutableString alloc] init];
 
 884     int bindingCount = 0;
 
 885     for (NSUInteger i = 0; i < columnNames.count; i++) {
 
 886         id value = valuesByColumnName[columnNames[i]];
 
 887         [whereSQL appendString:columnNames[i]];
 
 888         if (!value || [[NSNull null] isEqual:value]) {
 
 889             [whereSQL appendString:@" is NULL"];
 
 891             values[bindingCount++] = value;
 
 892             [whereSQL appendString:@"=?"];
 
 894         if (i != columnNames.count-1) {
 
 895             [whereSQL appendString:@" AND "];
 
 898     [self deleteFrom:tableName where:whereSQL bindings:values];
 
 901 - (void)deleteFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
 
 902     NSString *SQL = [NSString stringWithFormat:@"delete from %@ where %@", tableName, whereSQL];
 
 904     SFSQLiteStatement *statement = [self statementForSQL:SQL];
 
 905     [statement bindValues:bindings];
 
 910 - (NSString *)_tableNameForClass:(Class)objectClass {
 
 911     NSString *className = [objectClass SFSQLiteClassName];
 
 912     if (![className hasPrefix:_objectClassPrefix]) {
 
 913         secerror("sfsqlite: Object class \"%@\" does not have prefix \"%@\"", className, _objectClassPrefix);
 
 916     return [className substringFromIndex:_objectClassPrefix.length];
 
 919 - (SInt32)dbUserVersion {
 
 920     SInt32 userVersion = 0;
 
 921     SFSQLiteStatement *statement = [self statementForSQL:@"pragma user_version"];
 
 922     while ([statement step]) {
 
 923         userVersion = [statement intAtIndex:0];
 
 930 - (SInt32)autoVacuumSetting {
 
 931     SInt32 vacuumMode = 0;
 
 932     SFSQLiteStatement *statement = [self statementForSQL:@"pragma auto_vacuum"];
 
 933     while ([statement step]) {
 
 934         vacuumMode = [statement intAtIndex:0];