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
305 NSFileManager* fm = [NSFileManager defaultManager];
306 [fm setAttributes:@{NSFilePosixPermissions : [NSNumber numberWithShort:0666]}
309 [fm setAttributes:@{NSFilePosixPermissions : [NSNumber numberWithShort:0666]}
310 ofItemAtPath:[NSString stringWithFormat:@"%@-wal",_path]
312 [fm setAttributes:@{NSFilePosixPermissions : [NSNumber numberWithShort:0666]}
313 ofItemAtPath:[NSString stringWithFormat:@"%@-shm",_path]
318 - (BOOL)openWithError:(NSError **)error {
321 NSString *dbSchemaVersion, *dir;
323 NS_VALID_UNTIL_END_OF_SCOPE NSString *arcSafePath = _path;
325 if (_openCount > 0) {
326 NSAssert(_db != NULL, @"Missing handle for open cache db");
332 // Create the directory for the cache.
333 dir = [_path stringByDeletingLastPathComponent];
334 if (!SecCreateDirectoryAtPath(dir, &localError)) {
338 int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
340 flags |= SQLITE_OPEN_FILEPROTECTION_COMPLETEUNTILFIRSTUSERAUTHENTICATION;
342 int rc = sqlite3_open_v2([arcSafePath fileSystemRepresentation], &_db, flags, NULL);
343 if (rc != SQLITE_OK) {
344 localError = [NSError errorWithDomain:NSCocoaErrorDomain code:rc userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening db at %@, rc=%d(0x%x)", _path, rc, rc]}];
348 // Filesystem foo for multiple daemons from different users
349 [self attemptProperDatabasePermissions];
351 sqlite3_extended_result_codes(_db, 1);
352 rc = sqlite3_busy_timeout(_db, kSFSQLiteBusyTimeout);
353 if (rc != SQLITE_OK) {
357 // You don't argue with the Ben: rdar://12685305
358 if (![self executeSQL:@"pragma journal_mode = WAL"]) {
361 if (![self executeSQL:@"pragma synchronous = %@", [self _synchronousModeString]]) {
364 if ([self autoVacuumSetting] != kSFSQLiteAutoVacuumFull) {
365 /* After changing the auto_vacuum setting the DB must be vacuumed */
366 if (![self executeSQL:@"pragma auto_vacuum = FULL"] || ![self executeSQL:@"VACUUM"]) {
371 // rdar://problem/32168789
372 // [self executeSQL:@"pragma foreign_keys = 1"];
374 // Initialize the db within a transaction in case there is a crash between creating the schema and setting the
375 // schema version, and to avoid multiple threads trying to re-create the db at once.
378 // Create the Properties table before trying to read the schema version from it. If the Properties table doesn't
379 // exist we can't prepare a statement to access it.
380 results = [self select:@[@"name"] from:@"sqlite_master" where:@"type = ? AND name = ?" bindings:@[@"table", @"Properties"]];
381 if (!results.count) {
382 [self executeSQL:kSFSQLiteCreatePropertiesTableSQL];
385 // Check the schema version and create or re-create the db if needed.
387 dbSchemaVersion = [self propertyForKey:kSFSQLiteSchemaVersionKey];
388 SInt32 dbUserVersion = [self dbUserVersion];
390 if (!dbSchemaVersion) {
391 // The schema version isn't set so the db was just created or we failed to initialize it previously.
393 } else if (![dbSchemaVersion isEqualToString:self.schemaVersion]
394 || (self.userVersion && dbUserVersion != self.userVersion)) {
396 if (self.delegate && [self.delegate migrateDatabase:self fromVersion:dbUserVersion]) {
401 // 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.
402 [self removeAllStatements];
403 [self dropAllTables];
409 [self executeSQL:kSFSQLiteCreatePropertiesTableSQL];
410 [self executeSQL:@"%@", self.schema];
411 NSString *createdDateString = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSinceReferenceDate]];
412 [self setProperty:createdDateString forKey:kSFSQLiteCreatedDateKey];
418 // TODO: <rdar://problem/33115830> Resolve Race Condition When Setting 'userVersion/schemaVersion' in SFSQLite
419 if ([self.unitTestOverrides[@"RacyUserVersionUpdate"] isEqual:@YES]) {
425 if (create || _hasMigrated) {
426 [self setProperty:self.schemaVersion forKey:kSFSQLiteSchemaVersionKey];
427 if (self.userVersion) {
428 [self executeSQL:@"pragma user_version = %ld", (long)self.userVersion];
437 sqlite3_close_v2(_db);
441 if (!success && error) {
443 localError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening db at %@", _path]}];
452 if (![self openWithError:&error] && !(error && error.code == SQLITE_AUTH)) {
453 secerror("sfsqlite: Error opening db at %@: %@", self.path, error);
460 if (_openCount > 0) {
461 if (_openCount == 1) {
462 NSAssert(_db != NULL, @"Missing handle for open cache db");
464 [self removeAllStatements];
466 if (sqlite3_close(_db)) {
467 secerror("sfsqlite: Error closing database");
477 NSAssert(_openCount == 0, @"Trying to remove db at: %@ while it is open", _path);
478 [[NSFileManager defaultManager] removeItemAtPath:_path error:nil];
479 for (NSString *suffix in SFSQLiteJournalSuffixes()) {
480 [[NSFileManager defaultManager] removeItemAtPath:[_path stringByAppendingString:suffix] error:nil];
485 [self executeSQL:@"begin exclusive"];
489 [self executeSQL:@"end"];
493 [self executeSQL:@"rollback"];
497 [self executeSQL:@"analyze"];
501 [self executeSQL:@"vacuum"];
504 - (SFSQLiteRowID)lastInsertRowID {
506 secerror("sfsqlite: Database is closed");
510 return sqlite3_last_insert_rowid(_db);
516 secerror("sfsqlite: Database is closed");
520 return sqlite3_changes(_db);
523 - (BOOL)executeSQL:(NSString *)format, ... {
525 va_start(args, format);
526 BOOL result = [self executeSQL:format arguments:args];
531 - (BOOL)executeSQL:(NSString *)format arguments:(va_list)args {
532 NS_VALID_UNTIL_END_OF_SCOPE NSString *SQL = [[NSString alloc] initWithFormat:format arguments:args];
534 secerror("sfsqlite: Database is closed");
537 int execRet = sqlite3_exec(_db, [SQL UTF8String], NULL, NULL, NULL);
538 if (execRet != SQLITE_OK) {
539 if (execRet != SQLITE_AUTH && execRet != SQLITE_READONLY) {
540 secerror("sfsqlite: Error executing SQL: \"%@\" (%d)", SQL, execRet);
548 - (SFSQLiteStatement *)statementForSQL:(NSString *)SQL {
550 secerror("sfsqlite: Database is closed");
554 SFSQLiteStatement *statement = _statementsBySQL[SQL];
556 NSAssert(statement.isReset, @"Statement not reset after last use: \"%@\"", SQL);
558 sqlite3_stmt *handle = NULL;
559 NS_VALID_UNTIL_END_OF_SCOPE NSString *arcSafeSQL = SQL;
560 if (sqlite3_prepare_v2(_db, [arcSafeSQL UTF8String], -1, &handle, NULL)) {
561 secerror("Error preparing statement: %@", SQL);
565 statement = [[SFSQLiteStatement alloc] initWithSQLite:self SQL:SQL handle:handle];
566 _statementsBySQL[SQL] = statement;
572 - (void)removeAllStatements {
573 [[_statementsBySQL allValues] makeObjectsPerformSelector:@selector(finalizeStatement)];
574 [_statementsBySQL removeAllObjects];
577 - (NSArray *)allTableNames {
578 NSMutableArray *tableNames = [[NSMutableArray alloc] init];
580 SFSQLiteStatement *statement = [self statementForSQL:@"select name from sqlite_master where type = 'table'"];
581 while ([statement step]) {
582 NSString *name = [statement textAtIndex:0];
583 [tableNames addObject:name];
590 - (void)dropAllTables {
591 for (NSString *tableName in [self allTableNames]) {
592 [self executeSQL:@"drop table %@", tableName];
596 - (NSString *)propertyForKey:(NSString *)key {
598 secerror("SFSQLite: attempt to retrieve property without a key");
602 NSString *value = nil;
604 SFSQLiteStatement *statement = [self statementForSQL:@"select value from Properties where key = ?"];
605 [statement bindText:key atIndex:0];
606 if ([statement step]) {
607 value = [statement textAtIndex:0];
614 - (void)setProperty:(NSString *)value forKey:(NSString *)key {
616 secerror("SFSQLite: attempt to set property without a key");
621 SFSQLiteStatement *statement = [self statementForSQL:@"insert or replace into Properties (key, value) values (?,?)"];
622 [statement bindText:key atIndex:0];
623 [statement bindText:value atIndex:1];
627 [self removePropertyForKey:key];
631 - (NSDateFormatter *)dateFormatter {
632 if (!_dateFormatter) {
633 NSDateFormatter* dateFormatter = [NSDateFormatter new];
634 dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZ";
635 _dateFormatter = dateFormatter;
637 return _dateFormatter;
640 - (NSDateFormatter *)oldDateFormatter {
641 if (!_oldDateFormatter) {
642 NSDateFormatter* dateFormatter = [NSDateFormatter new];
643 dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZZZZZ";
644 _oldDateFormatter = dateFormatter;
646 return _oldDateFormatter;
649 - (NSDate *)datePropertyForKey:(NSString *)key {
650 NSString *dateStr = [self propertyForKey:key];
651 if (dateStr.length) {
652 NSDate *date = [self.dateFormatter dateFromString:dateStr];
654 date = [self.oldDateFormatter dateFromString:dateStr];
661 - (void)setDateProperty:(NSDate *)value forKey:(NSString *)key {
662 NSString *dateStr = nil;
664 dateStr = [self.dateFormatter stringFromDate:value];
666 [self setProperty:dateStr forKey:key];
669 - (void)removePropertyForKey:(NSString *)key {
674 SFSQLiteStatement *statement = [self statementForSQL:@"delete from Properties where key = ?"];
675 [statement bindText:key atIndex:0];
680 - (NSDate *)creationDate {
681 return [NSDate dateWithTimeIntervalSinceReferenceDate:[[self propertyForKey:kSFSQLiteCreatedDateKey] floatValue]];
684 // https://sqlite.org/pragma.html#pragma_table_info
685 - (NSSet<NSString*> *)columnNamesForTable:(NSString*)tableName {
686 SFSQLiteStatement *statement = [self statementForSQL:[NSString stringWithFormat:@"pragma table_info(%@)", tableName]];
687 NSMutableSet<NSString*>* columnNames = [[NSMutableSet alloc] init];
688 while ([statement step]) {
689 [columnNames addObject:[statement textAtIndex:1]];
695 - (NSArray *)select:(NSArray *)columns from:(NSString *)tableName {
696 return [self select:columns from:tableName where:nil bindings:nil];
699 - (NSArray *)select:(NSArray *)columns from:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
700 NSMutableArray *results = [[NSMutableArray alloc] init];
702 NSMutableString *SQL = [NSMutableString stringWithFormat:@"select %@ from %@", [columns componentsJoinedByString:@", "], tableName];
704 [SQL appendFormat:@" where %@", whereSQL];
707 SFSQLiteStatement *statement = [self statementForSQL:SQL];
708 [statement bindValues:bindings];
709 while ([statement step]) {
710 [results addObject:[statement allObjectsByColumnName]];
717 - (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 {
719 NSMutableString *SQL = [[NSMutableString alloc] init];
720 NSString *columnsString = @"*";
721 if ([columns count]) columnsString = [columns componentsJoinedByString:@", "];
722 [SQL appendFormat:@"select %@ from %@", columnsString, tableName];
724 if (whereSQL.length) {
725 [SQL appendFormat:@" where %@", whereSQL];
728 NSString *orderByString = [orderBy componentsJoinedByString:@", "];
729 [SQL appendFormat:@" order by %@", orderByString];
732 [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
735 SFSQLiteStatement *statement = [self statementForSQL:SQL];
736 [statement bindValues:bindings];
739 if (![statement step]) {
742 NSDictionary *stepResult = [statement allObjectsByColumnName];
745 block(stepResult, &stop);
756 - (void)selectFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings orderBy:(NSArray *)orderBy limit:(NSNumber *)limit block:(void (^)(NSDictionary *resultDictionary, BOOL *stop))block {
758 NSMutableString *SQL = [[NSMutableString alloc] init];
759 [SQL appendFormat:@"select * from %@", tableName];
761 if (whereSQL.length) {
762 [SQL appendFormat:@" where %@", whereSQL];
765 NSString *orderByString = [orderBy componentsJoinedByString:@", "];
766 [SQL appendFormat:@" order by %@", orderByString];
769 [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
772 SFSQLiteStatement *statement = [self statementForSQL:SQL];
773 [statement bindValues:bindings];
776 if (![statement step]) {
779 NSDictionary *stepResult = [statement allObjectsByColumnName];
782 block(stepResult, &stop);
793 - (NSArray *)selectFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings limit:(NSNumber *)limit {
794 NSMutableString *SQL = [[NSMutableString alloc] init];
795 [SQL appendFormat:@"select * from %@", tableName];
797 if (whereSQL.length) {
798 [SQL appendFormat:@" where %@", whereSQL];
801 [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
804 NSMutableArray *results = [[NSMutableArray alloc] init];
806 SFSQLiteStatement *statement = [self statementForSQL:SQL];
807 [statement bindValues:bindings];
808 while ([statement step]) {
809 [results addObject:[statement allObjectsByColumnName]];
816 - (void)update:(NSString *)tableName set:(NSString *)setSQL where:(NSString *)whereSQL bindings:(NSArray *)whereBindings limit:(NSNumber *)limit {
817 if (![setSQL length]) {
821 NSMutableString *SQL = [[NSMutableString alloc] init];
822 [SQL appendFormat:@"update %@", tableName];
824 [SQL appendFormat:@" set %@", setSQL];
825 if (whereSQL.length) {
826 [SQL appendFormat:@" where %@", whereSQL];
829 [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
832 SFSQLiteStatement *statement = [self statementForSQL:SQL];
833 [statement bindValues:whereBindings];
834 while ([statement step]) {
839 - (NSArray *)selectAllFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
840 return [self selectFrom:tableName where:whereSQL bindings:bindings limit:nil];
843 - (NSUInteger)selectCountFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
844 NSArray *results = [self select:@[@"count(*) as n"] from:tableName where:whereSQL bindings:bindings];
845 return [results[0][@"n"] unsignedIntegerValue];
848 - (SFSQLiteRowID)insertOrReplaceInto:(NSString *)tableName values:(NSDictionary *)valuesByColumnName {
849 NSArray *columnNames = [[valuesByColumnName allKeys] sortedArrayUsingSelector:@selector(compare:)];
850 NSMutableArray *values = [[NSMutableArray alloc] init];
851 for (NSUInteger i = 0; i < columnNames.count; i++) {
852 values[i] = valuesByColumnName[columnNames[i]];
855 NSMutableString *SQL = [[NSMutableString alloc] initWithString:@"insert or replace into "];
856 [SQL appendString:tableName];
857 [SQL appendString:@" ("];
858 for (NSUInteger i = 0; i < columnNames.count; i++) {
859 [SQL appendString:columnNames[i]];
860 if (i != columnNames.count-1) {
861 [SQL appendString:@","];
864 [SQL appendString:@") values ("];
865 for (NSUInteger i = 0; i < columnNames.count; i++) {
866 if (i != columnNames.count-1) {
867 [SQL appendString:@"?,"];
869 [SQL appendString:@"?"];
872 [SQL appendString:@")"];
874 SFSQLiteStatement *statement = [self statementForSQL:SQL];
875 [statement bindValues:values];
879 return [self lastInsertRowID];
882 - (void)deleteFrom:(NSString *)tableName matchingValues:(NSDictionary *)valuesByColumnName {
883 NSArray *columnNames = [[valuesByColumnName allKeys] sortedArrayUsingSelector:@selector(compare:)];
884 NSMutableArray *values = [[NSMutableArray alloc] init];
885 NSMutableString *whereSQL = [[NSMutableString alloc] init];
886 int bindingCount = 0;
887 for (NSUInteger i = 0; i < columnNames.count; i++) {
888 id value = valuesByColumnName[columnNames[i]];
889 [whereSQL appendString:columnNames[i]];
890 if (!value || [[NSNull null] isEqual:value]) {
891 [whereSQL appendString:@" is NULL"];
893 values[bindingCount++] = value;
894 [whereSQL appendString:@"=?"];
896 if (i != columnNames.count-1) {
897 [whereSQL appendString:@" AND "];
900 [self deleteFrom:tableName where:whereSQL bindings:values];
903 - (void)deleteFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
904 NSString *SQL = [NSString stringWithFormat:@"delete from %@ where %@", tableName, whereSQL];
906 SFSQLiteStatement *statement = [self statementForSQL:SQL];
907 [statement bindValues:bindings];
912 - (NSString *)_tableNameForClass:(Class)objectClass {
913 NSString *className = [objectClass SFSQLiteClassName];
914 if (![className hasPrefix:_objectClassPrefix]) {
915 secerror("sfsqlite: Object class \"%@\" does not have prefix \"%@\"", className, _objectClassPrefix);
918 return [className substringFromIndex:_objectClassPrefix.length];
921 - (SInt32)dbUserVersion {
922 SInt32 userVersion = 0;
923 SFSQLiteStatement *statement = [self statementForSQL:@"pragma user_version"];
924 while ([statement step]) {
925 userVersion = [statement intAtIndex:0];
932 - (SInt32)autoVacuumSetting {
933 SInt32 vacuumMode = 0;
934 SFSQLiteStatement *statement = [self statementForSQL:@"pragma auto_vacuum"];
935 while ([statement step]) {
936 vacuumMode = [statement intAtIndex:0];