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 // Vaccuum our databases approximately once a week
36 #define kCKSQLVacuumInterval ((60*60*24)*7)
37 #define kSFSQLiteLastVacuumKey @"LastVacuum"
39 #define kSFSQLiteSchemaVersionKey @"SchemaVersion"
40 #define kSFSQLiteCreatedDateKey @"Created"
42 static NSString *const kSFSQLiteCreatePropertiesTableSQL =
43 @"create table if not exists Properties (\n"
44 @" key text primary key,\n"
49 NSArray *SFSQLiteJournalSuffixes() {
50 return @[@"-journal", @"-wal", @"-shm"];
53 @interface NSObject (SFSQLiteAdditions)
54 + (NSString *)SFSQLiteClassName;
57 @implementation NSObject (SFSQLiteAdditions)
58 + (NSString *)SFSQLiteClassName {
59 return NSStringFromClass(self);
63 @interface SFSQLite ()
65 @property (nonatomic, assign) sqlite3 *db;
66 @property (nonatomic, assign) NSUInteger openCount;
67 @property (nonatomic, assign) BOOL corrupt;
68 @property (nonatomic, readonly, strong) NSMutableDictionary *statementsBySQL;
69 @property (nonatomic, strong) NSDateFormatter *dateFormatter;
73 static char intToHexChar(uint8_t i)
75 return i >= 10 ? 'a' + i - 10 : '0' + i;
78 static char *SecHexCharFromBytes(const uint8_t *bytes, NSUInteger length, NSUInteger *outlen) {
79 // Fudge the math a bit on the assert because we don't want a 1GB string anyway
80 if (length > (NSUIntegerMax / 3)) {
83 char *hex = calloc(1, length * 2 * 9 / 8); // 9/8 so we can inline ' ' between every 8 character sequence
88 for (i = 0; length > 4; i += 4, length -= 4) {
89 for (NSUInteger offset = 0; offset < 4; offset++) {
90 *destPtr++ = intToHexChar((bytes[i+offset] & 0xF0) >> 4);
91 *destPtr++ = intToHexChar(bytes[i+offset] & 0x0F);
96 /* Using the same i from the above loop */
97 for (; length > 0; i++, length--) {
98 *destPtr++ = intToHexChar((bytes[i] & 0xF0) >> 4);
99 *destPtr++ = intToHexChar(bytes[i] & 0x0F);
102 if (outlen) *outlen = destPtr - hex;
107 static BOOL SecCreateDirectoryAtPath(NSString *path, NSError **error) {
110 NSFileManager *fileManager = [NSFileManager defaultManager];
112 if (![fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&localError]) {
113 if (![localError.domain isEqualToString:NSCocoaErrorDomain] || localError.code != NSFileWriteFileExistsError) {
120 NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:&localError];
121 if (![attributes[NSFileProtectionKey] isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication]) {
122 [fileManager setAttributes:@{ NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication }
123 ofItemAtPath:path error:nil];
128 if (error) *error = localError;
133 @implementation NSData (CKUtilsAdditions)
135 - (NSString *)CKHexString {
136 NSUInteger hexLen = 0;
137 NS_VALID_UNTIL_END_OF_SCOPE NSData *arcSafeSelf = self;
138 char *hex = SecHexCharFromBytes([arcSafeSelf bytes], [arcSafeSelf length], &hexLen);
139 return [[NSString alloc] initWithBytesNoCopy:hex length:hexLen encoding:NSASCIIStringEncoding freeWhenDone:YES];
142 - (NSString *)CKLowercaseHexStringWithoutSpaces {
143 NSMutableString *retVal = [[self CKHexString] mutableCopy];
144 [retVal replaceOccurrencesOfString:@" " withString:@"" options:0 range:NSMakeRange(0, [retVal length])];
148 - (NSString *)CKUppercaseHexStringWithoutSpaces {
149 NSMutableString *retVal = [[[self CKHexString] uppercaseString] mutableCopy];
150 [retVal replaceOccurrencesOfString:@" " withString:@"" options:0 range:NSMakeRange(0, [retVal length])];
154 + (NSData *)CKDataWithHexString:(NSString *)hexString stringIsUppercase:(BOOL)stringIsUppercase {
155 NSMutableData *retVal = [[NSMutableData alloc] init];
156 NSCharacterSet *hexCharacterSet = nil;
158 if (stringIsUppercase) {
159 hexCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"];
162 hexCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdef"];
167 for (i = 0; i < [hexString length] ; ) {
168 BOOL validFirstByte = NO;
169 BOOL validSecondByte = NO;
170 unichar firstByte = 0;
171 unichar secondByte = 0;
173 for ( ; i < [hexString length]; i++) {
174 firstByte = [hexString characterAtIndex:i];
175 if ([hexCharacterSet characterIsMember:firstByte]) {
177 validFirstByte = YES;
181 for ( ; i < [hexString length]; i++) {
182 secondByte = [hexString characterAtIndex:i];
183 if ([hexCharacterSet characterIsMember:secondByte]) {
185 validSecondByte = YES;
189 if (!validFirstByte || !validSecondByte) {
192 if ((firstByte >= '0') && (firstByte <= '9')) {
195 firstByte = firstByte - aChar + 10;
197 if ((secondByte >= '0') && (secondByte <= '9')) {
200 secondByte = secondByte - aChar + 10;
202 char totalByteValue = (char)((firstByte << 4) + secondByte);
204 [retVal appendBytes:&totalByteValue length:1];
210 + (NSData *)CKDataWithHexString:(NSString *)hexString {
211 return [self CKDataWithHexString:hexString stringIsUppercase:NO];
216 @implementation SFSQLite
218 @synthesize delegate = _delegate;
219 @synthesize path = _path;
220 @synthesize schema = _schema;
221 @synthesize schemaVersion = _schemaVersion;
222 @synthesize objectClassPrefix = _objectClassPrefix;
223 @synthesize userVersion = _userVersion;
224 @synthesize synchronousMode = _synchronousMode;
225 @synthesize hasMigrated = _hasMigrated;
226 @synthesize shouldVacuum = _shouldVacuum;
227 @synthesize traced = _traced;
228 @synthesize db = _db;
229 @synthesize openCount = _openCount;
230 @synthesize corrupt = _corrupt;
231 @synthesize statementsBySQL = _statementsBySQL;
232 @synthesize dateFormatter = _dateFormatter;
234 @synthesize unitTestOverrides = _unitTestOverrides;
237 - (instancetype)initWithPath:(NSString *)path schema:(NSString *)schema {
238 if ((self = [super init])) {
239 NSAssert([path length], @"Can't init a database with a zero-length path");
242 _schemaVersion = [self _createSchemaHash];
243 _statementsBySQL = [[NSMutableDictionary alloc] init];
244 _objectClassPrefix = @"CK";
245 _synchronousMode = SFSQLiteSynchronousModeNormal;
258 - (SInt32)userVersion {
260 return self.delegate.userVersion;
265 - (NSString *)_synchronousModeString {
266 switch (self.synchronousMode) {
267 case SFSQLiteSynchronousModeOff:
269 case SFSQLiteSynchronousModeFull:
271 case SFSQLiteSynchronousModeNormal:
274 assert(0 && "Unknown synchronous mode");
279 - (NSString *)_createSchemaHash {
280 unsigned char hashBuffer[CC_SHA256_DIGEST_LENGTH] = {0};
281 NSData *hashData = [NSData dataWithBytesNoCopy:hashBuffer length:CC_SHA256_DIGEST_LENGTH freeWhenDone:NO];
282 NS_VALID_UNTIL_END_OF_SCOPE NSData *schemaData = [self.schema dataUsingEncoding:NSUTF8StringEncoding];
283 CC_SHA256([schemaData bytes], (CC_LONG)[schemaData length], hashBuffer);
284 return [hashData CKUppercaseHexStringWithoutSpaces];
291 - (void)_periodicVacuum {
292 // "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.
293 // 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.
294 // In fact, because it moves pages around within the file, auto-vacuum can actually make fragmentation worse."
295 // https://sqlite.org/pragma.html#pragma_auto_vacuum
296 NSDate *lastVacuumDate = [NSDate dateWithTimeIntervalSinceReferenceDate:[[self propertyForKey:kSFSQLiteLastVacuumKey] floatValue]];
297 if ([lastVacuumDate timeIntervalSinceNow] < -(kCKSQLVacuumInterval)) {
299 os_transaction_t transaction = os_transaction_create("SFSQLITE DB Vacuum");
300 secnotice("SFSQLITE", "performing periodic vacuum");
301 [self executeSQL:@"VACUUM"];
302 (void)transaction; // dead store
304 NSString *vacuumDateString = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSinceReferenceDate]];
305 [self setProperty:vacuumDateString forKey:kSFSQLiteLastVacuumKey];
311 Best-effort attempts to set/correct filesystem permissions.
312 May fail when we don't own DB which means we must wait for them to update permissions,
313 or file does not exist yet which is okay because db will exist and the aux files inherit permissions
315 - (void)attemptProperDatabasePermissions
318 NSFileManager* fm = [NSFileManager defaultManager];
319 [fm setAttributes:@{NSFilePosixPermissions : [NSNumber numberWithShort:0666]}
322 [fm setAttributes:@{NSFilePosixPermissions : [NSNumber numberWithShort:0666]}
323 ofItemAtPath:[NSString stringWithFormat:@"%@-wal",_path]
325 [fm setAttributes:@{NSFilePosixPermissions : [NSNumber numberWithShort:0666]}
326 ofItemAtPath:[NSString stringWithFormat:@"%@-shm",_path]
331 - (BOOL)openWithError:(NSError **)error {
334 NSString *dbSchemaVersion, *dir;
336 NS_VALID_UNTIL_END_OF_SCOPE NSString *arcSafePath = _path;
338 if (_openCount > 0) {
339 NSAssert(_db != NULL, @"Missing handle for open cache db");
345 // Create the directory for the cache.
346 dir = [_path stringByDeletingLastPathComponent];
347 if (!SecCreateDirectoryAtPath(dir, &localError)) {
351 int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
353 flags |= SQLITE_OPEN_FILEPROTECTION_COMPLETEUNTILFIRSTUSERAUTHENTICATION;
355 int rc = sqlite3_open_v2([arcSafePath fileSystemRepresentation], &_db, flags, NULL);
356 if (rc != SQLITE_OK) {
357 localError = [NSError errorWithDomain:NSCocoaErrorDomain code:rc userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening db at %@, rc=%d(0x%x)", _path, rc, rc]}];
361 // Filesystem foo for multiple daemons from different users
362 [self attemptProperDatabasePermissions];
364 sqlite3_extended_result_codes(_db, 1);
365 rc = sqlite3_busy_timeout(_db, kSFSQLiteBusyTimeout);
366 if (rc != SQLITE_OK) {
370 // You don't argue with the Ben: rdar://12685305
371 if (![self executeSQL:@"pragma journal_mode = WAL"]) {
374 if (![self executeSQL:@"pragma synchronous = %@", [self _synchronousModeString]]) {
377 if (![self executeSQL:@"pragma auto_vacuum = FULL"]) {
381 // rdar://problem/32168789
382 // [self executeSQL:@"pragma foreign_keys = 1"];
384 // Initialize the db within a transaction in case there is a crash between creating the schema and setting the
385 // schema version, and to avoid multiple threads trying to re-create the db at once.
388 // Create the Properties table before trying to read the schema version from it. If the Properties table doesn't
389 // exist we can't prepare a statement to access it.
390 results = [self select:@[@"name"] from:@"sqlite_master" where:@"type = ? AND name = ?" bindings:@[@"table", @"Properties"]];
391 if (!results.count) {
392 [self executeSQL:kSFSQLiteCreatePropertiesTableSQL];
395 // Check the schema version and create or re-create the db if needed.
397 dbSchemaVersion = [self propertyForKey:kSFSQLiteSchemaVersionKey];
398 SInt32 dbUserVersion = [self dbUserVersion];
400 if (!dbSchemaVersion) {
401 // The schema version isn't set so the db was just created or we failed to initialize it previously.
403 } else if (![dbSchemaVersion isEqualToString:self.schemaVersion]
404 || (self.userVersion && dbUserVersion != self.userVersion)) {
406 if (self.delegate && [self.delegate migrateDatabase:self fromVersion:dbUserVersion]) {
411 // 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.
412 [self removeAllStatements];
413 [self dropAllTables];
419 [self executeSQL:kSFSQLiteCreatePropertiesTableSQL];
420 [self executeSQL:@"%@", self.schema];
421 NSString *createdDateString = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSinceReferenceDate]];
422 [self setProperty:createdDateString forKey:kSFSQLiteCreatedDateKey];
428 // TODO: <rdar://problem/33115830> Resolve Race Condition When Setting 'userVersion/schemaVersion' in SFSQLite
429 if ([self.unitTestOverrides[@"RacyUserVersionUpdate"] isEqual:@YES]) {
435 if (self.shouldVacuum) [self _periodicVacuum];
437 if (create || _hasMigrated) {
438 [self setProperty:self.schemaVersion forKey:kSFSQLiteSchemaVersionKey];
439 if (self.userVersion) {
440 [self executeSQL:@"pragma user_version = %ld", (long)self.userVersion];
449 sqlite3_close_v2(_db);
453 if (!success && error) {
455 localError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening db at %@", _path]}];
464 if (![self openWithError:&error] && !(error && error.code == SQLITE_AUTH)) {
465 secerror("sfsqlite: Error opening db at %@: %@", self.path, error);
472 if (_openCount > 0) {
473 if (_openCount == 1) {
474 NSAssert(_db != NULL, @"Missing handle for open cache db");
476 [self removeAllStatements];
478 if (sqlite3_close(_db)) {
479 secerror("sfsqlite: Error closing database");
489 NSAssert(_openCount == 0, @"Trying to remove db at: %@ while it is open", _path);
490 [[NSFileManager defaultManager] removeItemAtPath:_path error:nil];
491 for (NSString *suffix in SFSQLiteJournalSuffixes()) {
492 [[NSFileManager defaultManager] removeItemAtPath:[_path stringByAppendingString:suffix] error:nil];
497 [self executeSQL:@"begin exclusive"];
501 [self executeSQL:@"end"];
505 [self executeSQL:@"rollback"];
509 [self executeSQL:@"analyze"];
513 [self executeSQL:@"vacuum"];
516 - (SFSQLiteRowID)lastInsertRowID {
518 secerror("sfsqlite: Database is closed");
522 return sqlite3_last_insert_rowid(_db);
528 secerror("sfsqlite: Database is closed");
532 return sqlite3_changes(_db);
535 - (BOOL)executeSQL:(NSString *)format, ... {
537 va_start(args, format);
538 BOOL result = [self executeSQL:format arguments:args];
543 - (BOOL)executeSQL:(NSString *)format arguments:(va_list)args {
544 NS_VALID_UNTIL_END_OF_SCOPE NSString *SQL = [[NSString alloc] initWithFormat:format arguments:args];
546 secerror("sfsqlite: Database is closed");
549 int execRet = sqlite3_exec(_db, [SQL UTF8String], NULL, NULL, NULL);
550 if (execRet != SQLITE_OK) {
551 if (execRet != SQLITE_AUTH && execRet != SQLITE_READONLY) {
552 secerror("sfsqlite: Error executing SQL: \"%@\" (%d)", SQL, execRet);
560 - (SFSQLiteStatement *)statementForSQL:(NSString *)SQL {
562 secerror("sfsqlite: Database is closed");
566 SFSQLiteStatement *statement = _statementsBySQL[SQL];
568 NSAssert(statement.isReset, @"Statement not reset after last use: \"%@\"", SQL);
570 sqlite3_stmt *handle = NULL;
571 NS_VALID_UNTIL_END_OF_SCOPE NSString *arcSafeSQL = SQL;
572 if (sqlite3_prepare_v2(_db, [arcSafeSQL UTF8String], -1, &handle, NULL)) {
573 secerror("Error preparing statement: %@", SQL);
577 statement = [[SFSQLiteStatement alloc] initWithSQLite:self SQL:SQL handle:handle];
578 _statementsBySQL[SQL] = statement;
584 - (void)removeAllStatements {
585 [[_statementsBySQL allValues] makeObjectsPerformSelector:@selector(finalizeStatement)];
586 [_statementsBySQL removeAllObjects];
589 - (NSArray *)allTableNames {
590 NSMutableArray *tableNames = [[NSMutableArray alloc] init];
592 SFSQLiteStatement *statement = [self statementForSQL:@"select name from sqlite_master where type = 'table'"];
593 while ([statement step]) {
594 NSString *name = [statement textAtIndex:0];
595 [tableNames addObject:name];
602 - (void)dropAllTables {
603 for (NSString *tableName in [self allTableNames]) {
604 [self executeSQL:@"drop table %@", tableName];
608 - (NSString *)propertyForKey:(NSString *)key {
609 NSAssert(key, @"Null key");
611 NSString *value = nil;
613 SFSQLiteStatement *statement = [self statementForSQL:@"select value from Properties where key = ?"];
614 [statement bindText:key atIndex:0];
615 if ([statement step]) {
616 value = [statement textAtIndex:0];
623 - (void)setProperty:(NSString *)value forKey:(NSString *)key {
624 NSAssert(key, @"Null key");
627 SFSQLiteStatement *statement = [self statementForSQL:@"insert or replace into Properties (key, value) values (?,?)"];
628 [statement bindText:key atIndex:0];
629 [statement bindText:value atIndex:1];
633 [self removePropertyForKey:key];
637 - (NSDateFormatter *)dateFormatter {
638 if (!_dateFormatter) {
639 NSDateFormatter* dateFormatter = [NSDateFormatter new];
640 dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZZZZZ";
641 _dateFormatter = dateFormatter;
643 return _dateFormatter;
646 - (NSDate *)datePropertyForKey:(NSString *)key {
647 NSString *dateStr = [self propertyForKey:key];
648 if (dateStr.length) {
649 return [self.dateFormatter dateFromString:dateStr];
654 - (void)setDateProperty:(NSDate *)value forKey:(NSString *)key {
655 NSString *dateStr = nil;
657 dateStr = [self.dateFormatter stringFromDate:value];
659 [self setProperty:dateStr forKey:key];
662 - (void)removePropertyForKey:(NSString *)key {
663 NSAssert(key, @"Null key");
665 SFSQLiteStatement *statement = [self statementForSQL:@"delete from Properties where key = ?"];
666 [statement bindText:key atIndex:0];
671 - (NSDate *)creationDate {
672 return [NSDate dateWithTimeIntervalSinceReferenceDate:[[self propertyForKey:kSFSQLiteCreatedDateKey] floatValue]];
675 // https://sqlite.org/pragma.html#pragma_table_info
676 - (NSSet<NSString*> *)columnNamesForTable:(NSString*)tableName {
677 SFSQLiteStatement *statement = [self statementForSQL:[NSString stringWithFormat:@"pragma table_info(%@)", tableName]];
678 NSMutableSet<NSString*>* columnNames = [[NSMutableSet alloc] init];
679 while ([statement step]) {
680 [columnNames addObject:[statement textAtIndex:1]];
686 - (NSArray *)select:(NSArray *)columns from:(NSString *)tableName {
687 return [self select:columns from:tableName where:nil bindings:nil];
690 - (NSArray *)select:(NSArray *)columns from:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
691 NSMutableArray *results = [[NSMutableArray alloc] init];
693 NSMutableString *SQL = [NSMutableString stringWithFormat:@"select %@ from %@", [columns componentsJoinedByString:@", "], tableName];
695 [SQL appendFormat:@" where %@", whereSQL];
698 SFSQLiteStatement *statement = [self statementForSQL:SQL];
699 [statement bindValues:bindings];
700 while ([statement step]) {
701 [results addObject:[statement allObjectsByColumnName]];
708 - (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 {
710 NSMutableString *SQL = [[NSMutableString alloc] init];
711 NSString *columnsString = @"*";
712 if ([columns count]) columnsString = [columns componentsJoinedByString:@", "];
713 [SQL appendFormat:@"select %@ from %@", columnsString, tableName];
715 if (whereSQL.length) {
716 [SQL appendFormat:@" where %@", whereSQL];
719 NSString *orderByString = [orderBy componentsJoinedByString:@", "];
720 [SQL appendFormat:@" order by %@", orderByString];
723 [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
726 SFSQLiteStatement *statement = [self statementForSQL:SQL];
727 [statement bindValues:bindings];
730 if (![statement step]) {
733 NSDictionary *stepResult = [statement allObjectsByColumnName];
736 block(stepResult, &stop);
747 - (void)selectFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings orderBy:(NSArray *)orderBy limit:(NSNumber *)limit block:(void (^)(NSDictionary *resultDictionary, BOOL *stop))block {
749 NSMutableString *SQL = [[NSMutableString alloc] init];
750 [SQL appendFormat:@"select * from %@", tableName];
752 if (whereSQL.length) {
753 [SQL appendFormat:@" where %@", whereSQL];
756 NSString *orderByString = [orderBy componentsJoinedByString:@", "];
757 [SQL appendFormat:@" order by %@", orderByString];
760 [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
763 SFSQLiteStatement *statement = [self statementForSQL:SQL];
764 [statement bindValues:bindings];
767 if (![statement step]) {
770 NSDictionary *stepResult = [statement allObjectsByColumnName];
773 block(stepResult, &stop);
784 - (NSArray *)selectFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings limit:(NSNumber *)limit {
785 NSMutableString *SQL = [[NSMutableString alloc] init];
786 [SQL appendFormat:@"select * from %@", tableName];
788 if (whereSQL.length) {
789 [SQL appendFormat:@" where %@", whereSQL];
792 [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
795 NSMutableArray *results = [[NSMutableArray alloc] init];
797 SFSQLiteStatement *statement = [self statementForSQL:SQL];
798 [statement bindValues:bindings];
799 while ([statement step]) {
800 [results addObject:[statement allObjectsByColumnName]];
807 - (void)update:(NSString *)tableName set:(NSString *)setSQL where:(NSString *)whereSQL bindings:(NSArray *)whereBindings limit:(NSNumber *)limit {
808 NSMutableString *SQL = [[NSMutableString alloc] init];
809 [SQL appendFormat:@"update %@", tableName];
811 NSAssert(setSQL.length > 0, @"null set expression");
813 [SQL appendFormat:@" set %@", setSQL];
814 if (whereSQL.length) {
815 [SQL appendFormat:@" where %@", whereSQL];
818 [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
821 SFSQLiteStatement *statement = [self statementForSQL:SQL];
822 [statement bindValues:whereBindings];
823 while ([statement step]) {
828 - (NSArray *)selectAllFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
829 return [self selectFrom:tableName where:whereSQL bindings:bindings limit:nil];
832 - (NSUInteger)selectCountFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
833 NSArray *results = [self select:@[@"count(*) as n"] from:tableName where:whereSQL bindings:bindings];
834 return [results[0][@"n"] unsignedIntegerValue];
837 - (SFSQLiteRowID)insertOrReplaceInto:(NSString *)tableName values:(NSDictionary *)valuesByColumnName {
838 NSArray *columnNames = [[valuesByColumnName allKeys] sortedArrayUsingSelector:@selector(compare:)];
839 NSMutableArray *values = [[NSMutableArray alloc] init];
840 for (NSUInteger i = 0; i < columnNames.count; i++) {
841 values[i] = valuesByColumnName[columnNames[i]];
844 NSMutableString *SQL = [[NSMutableString alloc] initWithString:@"insert or replace into "];
845 [SQL appendString:tableName];
846 [SQL appendString:@" ("];
847 for (NSUInteger i = 0; i < columnNames.count; i++) {
848 [SQL appendString:columnNames[i]];
849 if (i != columnNames.count-1) {
850 [SQL appendString:@","];
853 [SQL appendString:@") values ("];
854 for (NSUInteger i = 0; i < columnNames.count; i++) {
855 if (i != columnNames.count-1) {
856 [SQL appendString:@"?,"];
858 [SQL appendString:@"?"];
861 [SQL appendString:@")"];
863 SFSQLiteStatement *statement = [self statementForSQL:SQL];
864 [statement bindValues:values];
868 return [self lastInsertRowID];
871 - (void)deleteFrom:(NSString *)tableName matchingValues:(NSDictionary *)valuesByColumnName {
872 NSArray *columnNames = [[valuesByColumnName allKeys] sortedArrayUsingSelector:@selector(compare:)];
873 NSMutableArray *values = [[NSMutableArray alloc] init];
874 NSMutableString *whereSQL = [[NSMutableString alloc] init];
875 int bindingCount = 0;
876 for (NSUInteger i = 0; i < columnNames.count; i++) {
877 id value = valuesByColumnName[columnNames[i]];
878 [whereSQL appendString:columnNames[i]];
879 if (!value || [[NSNull null] isEqual:value]) {
880 [whereSQL appendString:@" is NULL"];
882 values[bindingCount++] = value;
883 [whereSQL appendString:@"=?"];
885 if (i != columnNames.count-1) {
886 [whereSQL appendString:@" AND "];
889 [self deleteFrom:tableName where:whereSQL bindings:values];
892 - (void)deleteFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
893 NSString *SQL = [NSString stringWithFormat:@"delete from %@ where %@", tableName, whereSQL];
895 SFSQLiteStatement *statement = [self statementForSQL:SQL];
896 [statement bindValues:bindings];
901 - (NSString *)_tableNameForClass:(Class)objectClass {
902 NSString *className = [objectClass SFSQLiteClassName];
903 if (![className hasPrefix:_objectClassPrefix]) {
904 secerror("sfsqlite: %@", [NSString stringWithFormat:@"Object class \"%@\" does not have prefix \"%@\"", className, _objectClassPrefix]);
907 return [className substringFromIndex:_objectClassPrefix.length];
910 - (SInt32)dbUserVersion {
911 SInt32 userVersion = 0;
912 SFSQLiteStatement *statement = [self statementForSQL:@"pragma user_version"];
913 while ([statement step]) {
914 userVersion = [statement intAtIndex:0];