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];