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