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>
31 #include <os/transaction_private.h>
33 #define kSFSQLiteBusyTimeout (5*60*1000)
35 #define kSFSQLiteSchemaVersionKey @"SchemaVersion"
36 #define kSFSQLiteCreatedDateKey @"Created"
37 #define kSFSQLiteAutoVacuumFull 1
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 traced = _traced;
224 @synthesize db = _db;
225 @synthesize openCount = _openCount;
226 @synthesize corrupt = _corrupt;
227 @synthesize statementsBySQL = _statementsBySQL;
228 @synthesize dateFormatter = _dateFormatter;
230 @synthesize unitTestOverrides = _unitTestOverrides;
233 - (instancetype)initWithPath:(NSString *)path schema:(NSString *)schema {
234 if (![path length]) {
235 seccritical("Cannot init db with empty path");
238 if (![schema length]) {
239 seccritical("Cannot init db without schema");
243 if ((self = [super init])) {
246 _schemaVersion = [self _createSchemaHash];
247 _statementsBySQL = [[NSMutableDictionary alloc] init];
248 _objectClassPrefix = @"CK";
249 _synchronousMode = SFSQLiteSynchronousModeNormal;
261 - (SInt32)userVersion {
263 return self.delegate.userVersion;
268 - (NSString *)_synchronousModeString {
269 switch (self.synchronousMode) {
270 case SFSQLiteSynchronousModeOff:
272 case SFSQLiteSynchronousModeFull:
274 case SFSQLiteSynchronousModeNormal:
277 assert(0 && "Unknown synchronous mode");
282 - (NSString *)_createSchemaHash {
283 unsigned char hashBuffer[CC_SHA256_DIGEST_LENGTH] = {0};
284 NSData *hashData = [NSData dataWithBytesNoCopy:hashBuffer length:CC_SHA256_DIGEST_LENGTH freeWhenDone:NO];
285 NS_VALID_UNTIL_END_OF_SCOPE NSData *schemaData = [self.schema dataUsingEncoding:NSUTF8StringEncoding];
286 CC_SHA256([schemaData bytes], (CC_LONG)[schemaData length], hashBuffer);
287 return [hashData CKUppercaseHexStringWithoutSpaces];
295 Best-effort attempts to set/correct filesystem permissions.
296 May fail when we don't own DB which means we must wait for them to update permissions,
297 or file does not exist yet which is okay because db will exist and the aux files inherit permissions
299 - (void)attemptProperDatabasePermissions
302 NSFileManager* fm = [NSFileManager defaultManager];
303 [fm setAttributes:@{NSFilePosixPermissions : [NSNumber numberWithShort:0666]}
306 [fm setAttributes:@{NSFilePosixPermissions : [NSNumber numberWithShort:0666]}
307 ofItemAtPath:[NSString stringWithFormat:@"%@-wal",_path]
309 [fm setAttributes:@{NSFilePosixPermissions : [NSNumber numberWithShort:0666]}
310 ofItemAtPath:[NSString stringWithFormat:@"%@-shm",_path]
315 - (BOOL)openWithError:(NSError **)error {
318 NSString *dbSchemaVersion, *dir;
320 NS_VALID_UNTIL_END_OF_SCOPE NSString *arcSafePath = _path;
322 if (_openCount > 0) {
323 NSAssert(_db != NULL, @"Missing handle for open cache db");
329 // Create the directory for the cache.
330 dir = [_path stringByDeletingLastPathComponent];
331 if (!SecCreateDirectoryAtPath(dir, &localError)) {
335 int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
337 flags |= SQLITE_OPEN_FILEPROTECTION_COMPLETEUNTILFIRSTUSERAUTHENTICATION;
339 int rc = sqlite3_open_v2([arcSafePath fileSystemRepresentation], &_db, flags, NULL);
340 if (rc != SQLITE_OK) {
341 localError = [NSError errorWithDomain:NSCocoaErrorDomain code:rc userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening db at %@, rc=%d(0x%x)", _path, rc, rc]}];
345 // Filesystem foo for multiple daemons from different users
346 [self attemptProperDatabasePermissions];
348 sqlite3_extended_result_codes(_db, 1);
349 rc = sqlite3_busy_timeout(_db, kSFSQLiteBusyTimeout);
350 if (rc != SQLITE_OK) {
354 // You don't argue with the Ben: rdar://12685305
355 if (![self executeSQL:@"pragma journal_mode = WAL"]) {
358 if (![self executeSQL:@"pragma synchronous = %@", [self _synchronousModeString]]) {
361 if ([self autoVacuumSetting] != kSFSQLiteAutoVacuumFull) {
362 /* After changing the auto_vacuum setting the DB must be vacuumed */
363 if (![self executeSQL:@"pragma auto_vacuum = FULL"] || ![self executeSQL:@"VACUUM"]) {
368 // rdar://problem/32168789
369 // [self executeSQL:@"pragma foreign_keys = 1"];
371 // Initialize the db within a transaction in case there is a crash between creating the schema and setting the
372 // schema version, and to avoid multiple threads trying to re-create the db at once.
375 // Create the Properties table before trying to read the schema version from it. If the Properties table doesn't
376 // exist we can't prepare a statement to access it.
377 results = [self select:@[@"name"] from:@"sqlite_master" where:@"type = ? AND name = ?" bindings:@[@"table", @"Properties"]];
378 if (!results.count) {
379 [self executeSQL:kSFSQLiteCreatePropertiesTableSQL];
382 // Check the schema version and create or re-create the db if needed.
384 dbSchemaVersion = [self propertyForKey:kSFSQLiteSchemaVersionKey];
385 SInt32 dbUserVersion = [self dbUserVersion];
387 if (!dbSchemaVersion) {
388 // The schema version isn't set so the db was just created or we failed to initialize it previously.
390 } else if (![dbSchemaVersion isEqualToString:self.schemaVersion]
391 || (self.userVersion && dbUserVersion != self.userVersion)) {
393 if (self.delegate && [self.delegate migrateDatabase:self fromVersion:dbUserVersion]) {
398 // 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.
399 [self removeAllStatements];
400 [self dropAllTables];
406 [self executeSQL:kSFSQLiteCreatePropertiesTableSQL];
407 [self executeSQL:@"%@", self.schema];
408 NSString *createdDateString = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSinceReferenceDate]];
409 [self setProperty:createdDateString forKey:kSFSQLiteCreatedDateKey];
415 // TODO: <rdar://problem/33115830> Resolve Race Condition When Setting 'userVersion/schemaVersion' in SFSQLite
416 if ([self.unitTestOverrides[@"RacyUserVersionUpdate"] isEqual:@YES]) {
422 if (create || _hasMigrated) {
423 [self setProperty:self.schemaVersion forKey:kSFSQLiteSchemaVersionKey];
424 if (self.userVersion) {
425 [self executeSQL:@"pragma user_version = %ld", (long)self.userVersion];
434 sqlite3_close_v2(_db);
438 if (!success && error) {
440 localError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening db at %@", _path]}];
449 if (![self openWithError:&error] && !(error && error.code == SQLITE_AUTH)) {
450 secerror("sfsqlite: Error opening db at %@: %@", self.path, error);
457 if (_openCount > 0) {
458 if (_openCount == 1) {
459 NSAssert(_db != NULL, @"Missing handle for open cache db");
461 [self removeAllStatements];
463 if (sqlite3_close(_db)) {
464 secerror("sfsqlite: Error closing database");
474 NSAssert(_openCount == 0, @"Trying to remove db at: %@ while it is open", _path);
475 [[NSFileManager defaultManager] removeItemAtPath:_path error:nil];
476 for (NSString *suffix in SFSQLiteJournalSuffixes()) {
477 [[NSFileManager defaultManager] removeItemAtPath:[_path stringByAppendingString:suffix] error:nil];
482 [self executeSQL:@"begin exclusive"];
486 [self executeSQL:@"end"];
490 [self executeSQL:@"rollback"];
494 [self executeSQL:@"analyze"];
498 [self executeSQL:@"vacuum"];
501 - (SFSQLiteRowID)lastInsertRowID {
503 secerror("sfsqlite: Database is closed");
507 return sqlite3_last_insert_rowid(_db);
513 secerror("sfsqlite: Database is closed");
517 return sqlite3_changes(_db);
520 - (BOOL)executeSQL:(NSString *)format, ... {
522 va_start(args, format);
523 BOOL result = [self executeSQL:format arguments:args];
528 - (BOOL)executeSQL:(NSString *)format arguments:(va_list)args {
529 NS_VALID_UNTIL_END_OF_SCOPE NSString *SQL = [[NSString alloc] initWithFormat:format arguments:args];
531 secerror("sfsqlite: Database is closed");
534 int execRet = sqlite3_exec(_db, [SQL UTF8String], NULL, NULL, NULL);
535 if (execRet != SQLITE_OK) {
536 if (execRet != SQLITE_AUTH && execRet != SQLITE_READONLY) {
537 secerror("sfsqlite: Error executing SQL: \"%@\" (%d)", SQL, execRet);
545 - (SFSQLiteStatement *)statementForSQL:(NSString *)SQL {
547 secerror("sfsqlite: Database is closed");
551 SFSQLiteStatement *statement = _statementsBySQL[SQL];
553 NSAssert(statement.isReset, @"Statement not reset after last use: \"%@\"", SQL);
555 sqlite3_stmt *handle = NULL;
556 NS_VALID_UNTIL_END_OF_SCOPE NSString *arcSafeSQL = SQL;
557 if (sqlite3_prepare_v2(_db, [arcSafeSQL UTF8String], -1, &handle, NULL)) {
558 secerror("Error preparing statement: %@", SQL);
562 statement = [[SFSQLiteStatement alloc] initWithSQLite:self SQL:SQL handle:handle];
563 _statementsBySQL[SQL] = statement;
569 - (void)removeAllStatements {
570 [[_statementsBySQL allValues] makeObjectsPerformSelector:@selector(finalizeStatement)];
571 [_statementsBySQL removeAllObjects];
574 - (NSArray *)allTableNames {
575 NSMutableArray *tableNames = [[NSMutableArray alloc] init];
577 SFSQLiteStatement *statement = [self statementForSQL:@"select name from sqlite_master where type = 'table'"];
578 while ([statement step]) {
579 NSString *name = [statement textAtIndex:0];
580 [tableNames addObject:name];
587 - (void)dropAllTables {
588 for (NSString *tableName in [self allTableNames]) {
589 [self executeSQL:@"drop table %@", tableName];
593 - (NSString *)propertyForKey:(NSString *)key {
595 secerror("SFSQLite: attempt to retrieve property without a key");
599 NSString *value = nil;
601 SFSQLiteStatement *statement = [self statementForSQL:@"select value from Properties where key = ?"];
602 [statement bindText:key atIndex:0];
603 if ([statement step]) {
604 value = [statement textAtIndex:0];
611 - (void)setProperty:(NSString *)value forKey:(NSString *)key {
613 secerror("SFSQLite: attempt to set property without a key");
618 SFSQLiteStatement *statement = [self statementForSQL:@"insert or replace into Properties (key, value) values (?,?)"];
619 [statement bindText:key atIndex:0];
620 [statement bindText:value atIndex:1];
624 [self removePropertyForKey:key];
628 - (NSDateFormatter *)dateFormatter {
629 if (!_dateFormatter) {
630 NSDateFormatter* dateFormatter = [NSDateFormatter new];
631 dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZZZZZ";
632 _dateFormatter = dateFormatter;
634 return _dateFormatter;
637 - (NSDate *)datePropertyForKey:(NSString *)key {
638 NSString *dateStr = [self propertyForKey:key];
639 if (dateStr.length) {
640 return [self.dateFormatter dateFromString:dateStr];
645 - (void)setDateProperty:(NSDate *)value forKey:(NSString *)key {
646 NSString *dateStr = nil;
648 dateStr = [self.dateFormatter stringFromDate:value];
650 [self setProperty:dateStr forKey:key];
653 - (void)removePropertyForKey:(NSString *)key {
658 SFSQLiteStatement *statement = [self statementForSQL:@"delete from Properties where key = ?"];
659 [statement bindText:key atIndex:0];
664 - (NSDate *)creationDate {
665 return [NSDate dateWithTimeIntervalSinceReferenceDate:[[self propertyForKey:kSFSQLiteCreatedDateKey] floatValue]];
668 // https://sqlite.org/pragma.html#pragma_table_info
669 - (NSSet<NSString*> *)columnNamesForTable:(NSString*)tableName {
670 SFSQLiteStatement *statement = [self statementForSQL:[NSString stringWithFormat:@"pragma table_info(%@)", tableName]];
671 NSMutableSet<NSString*>* columnNames = [[NSMutableSet alloc] init];
672 while ([statement step]) {
673 [columnNames addObject:[statement textAtIndex:1]];
679 - (NSArray *)select:(NSArray *)columns from:(NSString *)tableName {
680 return [self select:columns from:tableName where:nil bindings:nil];
683 - (NSArray *)select:(NSArray *)columns from:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
684 NSMutableArray *results = [[NSMutableArray alloc] init];
686 NSMutableString *SQL = [NSMutableString stringWithFormat:@"select %@ from %@", [columns componentsJoinedByString:@", "], tableName];
688 [SQL appendFormat:@" where %@", whereSQL];
691 SFSQLiteStatement *statement = [self statementForSQL:SQL];
692 [statement bindValues:bindings];
693 while ([statement step]) {
694 [results addObject:[statement allObjectsByColumnName]];
701 - (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 {
703 NSMutableString *SQL = [[NSMutableString alloc] init];
704 NSString *columnsString = @"*";
705 if ([columns count]) columnsString = [columns componentsJoinedByString:@", "];
706 [SQL appendFormat:@"select %@ from %@", columnsString, tableName];
708 if (whereSQL.length) {
709 [SQL appendFormat:@" where %@", whereSQL];
712 NSString *orderByString = [orderBy componentsJoinedByString:@", "];
713 [SQL appendFormat:@" order by %@", orderByString];
716 [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
719 SFSQLiteStatement *statement = [self statementForSQL:SQL];
720 [statement bindValues:bindings];
723 if (![statement step]) {
726 NSDictionary *stepResult = [statement allObjectsByColumnName];
729 block(stepResult, &stop);
740 - (void)selectFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings orderBy:(NSArray *)orderBy limit:(NSNumber *)limit block:(void (^)(NSDictionary *resultDictionary, BOOL *stop))block {
742 NSMutableString *SQL = [[NSMutableString alloc] init];
743 [SQL appendFormat:@"select * from %@", tableName];
745 if (whereSQL.length) {
746 [SQL appendFormat:@" where %@", whereSQL];
749 NSString *orderByString = [orderBy componentsJoinedByString:@", "];
750 [SQL appendFormat:@" order by %@", orderByString];
753 [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
756 SFSQLiteStatement *statement = [self statementForSQL:SQL];
757 [statement bindValues:bindings];
760 if (![statement step]) {
763 NSDictionary *stepResult = [statement allObjectsByColumnName];
766 block(stepResult, &stop);
777 - (NSArray *)selectFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings limit:(NSNumber *)limit {
778 NSMutableString *SQL = [[NSMutableString alloc] init];
779 [SQL appendFormat:@"select * from %@", tableName];
781 if (whereSQL.length) {
782 [SQL appendFormat:@" where %@", whereSQL];
785 [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
788 NSMutableArray *results = [[NSMutableArray alloc] init];
790 SFSQLiteStatement *statement = [self statementForSQL:SQL];
791 [statement bindValues:bindings];
792 while ([statement step]) {
793 [results addObject:[statement allObjectsByColumnName]];
800 - (void)update:(NSString *)tableName set:(NSString *)setSQL where:(NSString *)whereSQL bindings:(NSArray *)whereBindings limit:(NSNumber *)limit {
801 if (![setSQL length]) {
805 NSMutableString *SQL = [[NSMutableString alloc] init];
806 [SQL appendFormat:@"update %@", tableName];
808 [SQL appendFormat:@" set %@", setSQL];
809 if (whereSQL.length) {
810 [SQL appendFormat:@" where %@", whereSQL];
813 [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
816 SFSQLiteStatement *statement = [self statementForSQL:SQL];
817 [statement bindValues:whereBindings];
818 while ([statement step]) {
823 - (NSArray *)selectAllFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
824 return [self selectFrom:tableName where:whereSQL bindings:bindings limit:nil];
827 - (NSUInteger)selectCountFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
828 NSArray *results = [self select:@[@"count(*) as n"] from:tableName where:whereSQL bindings:bindings];
829 return [results[0][@"n"] unsignedIntegerValue];
832 - (SFSQLiteRowID)insertOrReplaceInto:(NSString *)tableName values:(NSDictionary *)valuesByColumnName {
833 NSArray *columnNames = [[valuesByColumnName allKeys] sortedArrayUsingSelector:@selector(compare:)];
834 NSMutableArray *values = [[NSMutableArray alloc] init];
835 for (NSUInteger i = 0; i < columnNames.count; i++) {
836 values[i] = valuesByColumnName[columnNames[i]];
839 NSMutableString *SQL = [[NSMutableString alloc] initWithString:@"insert or replace into "];
840 [SQL appendString:tableName];
841 [SQL appendString:@" ("];
842 for (NSUInteger i = 0; i < columnNames.count; i++) {
843 [SQL appendString:columnNames[i]];
844 if (i != columnNames.count-1) {
845 [SQL appendString:@","];
848 [SQL appendString:@") values ("];
849 for (NSUInteger i = 0; i < columnNames.count; i++) {
850 if (i != columnNames.count-1) {
851 [SQL appendString:@"?,"];
853 [SQL appendString:@"?"];
856 [SQL appendString:@")"];
858 SFSQLiteStatement *statement = [self statementForSQL:SQL];
859 [statement bindValues:values];
863 return [self lastInsertRowID];
866 - (void)deleteFrom:(NSString *)tableName matchingValues:(NSDictionary *)valuesByColumnName {
867 NSArray *columnNames = [[valuesByColumnName allKeys] sortedArrayUsingSelector:@selector(compare:)];
868 NSMutableArray *values = [[NSMutableArray alloc] init];
869 NSMutableString *whereSQL = [[NSMutableString alloc] init];
870 int bindingCount = 0;
871 for (NSUInteger i = 0; i < columnNames.count; i++) {
872 id value = valuesByColumnName[columnNames[i]];
873 [whereSQL appendString:columnNames[i]];
874 if (!value || [[NSNull null] isEqual:value]) {
875 [whereSQL appendString:@" is NULL"];
877 values[bindingCount++] = value;
878 [whereSQL appendString:@"=?"];
880 if (i != columnNames.count-1) {
881 [whereSQL appendString:@" AND "];
884 [self deleteFrom:tableName where:whereSQL bindings:values];
887 - (void)deleteFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
888 NSString *SQL = [NSString stringWithFormat:@"delete from %@ where %@", tableName, whereSQL];
890 SFSQLiteStatement *statement = [self statementForSQL:SQL];
891 [statement bindValues:bindings];
896 - (NSString *)_tableNameForClass:(Class)objectClass {
897 NSString *className = [objectClass SFSQLiteClassName];
898 if (![className hasPrefix:_objectClassPrefix]) {
899 secerror("sfsqlite: %@", [NSString stringWithFormat:@"Object class \"%@\" does not have prefix \"%@\"", className, _objectClassPrefix]);
902 return [className substringFromIndex:_objectClassPrefix.length];
905 - (SInt32)dbUserVersion {
906 SInt32 userVersion = 0;
907 SFSQLiteStatement *statement = [self statementForSQL:@"pragma user_version"];
908 while ([statement step]) {
909 userVersion = [statement intAtIndex:0];
916 - (SInt32)autoVacuumSetting {
917 SInt32 vacuumMode = 0;
918 SFSQLiteStatement *statement = [self statementForSQL:@"pragma auto_vacuum"];
919 while ([statement step]) {
920 vacuumMode = [statement intAtIndex:0];