--- /dev/null
+/*
+ * Copyright (c) 2017 Apple Inc. All Rights Reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ *
+ * @APPLE_LICENSE_HEADER_END@
+ */
+
+#import "SFSQLite.h"
+#import "SFSQLiteStatement.h"
+#include <sqlite3.h>
+#include <CommonCrypto/CommonDigest.h>
+
+
+#define kSFSQLiteBusyTimeout (5*60*1000)
+
+// Vaccuum our databases approximately once a week
+#define kCKSQLVacuumInterval ((60*60*24)*7)
+#define kSFSQLiteLastVacuumKey @"LastVacuum"
+
+#define kSFSQLiteSchemaVersionKey @"SchemaVersion"
+#define kSFSQLiteCreatedDateKey @"Created"
+
+static NSString *const kSFSQLiteCreatePropertiesTableSQL =
+ @"create table if not exists Properties (\n"
+ @" key text primary key,\n"
+ @" value text\n"
+ @");\n";
+
+@interface SFSQLiteError : NSObject
++ (void)raise:(NSString *)reason code:(int)code extended:(int)extended;
+@end
+
+
+NSArray *SFSQLiteJournalSuffixes() {
+ return @[@"-journal", @"-wal", @"-shm"];
+}
+
+@interface NSObject (SFSQLiteAdditions)
++ (NSString *)SFSQLiteClassName;
+@end
+
+@implementation NSObject (SFSQLiteAdditions)
++ (NSString *)SFSQLiteClassName {
+ return NSStringFromClass(self);
+}
+@end
+
+@interface SFSQLite ()
+
+@property (nonatomic, assign) sqlite3 *db;
+@property (nonatomic, assign) NSUInteger openCount;
+@property (nonatomic, assign) BOOL corrupt;
+@property (nonatomic, readonly, strong) NSMutableDictionary *statementsBySQL;
+@property (nonatomic, strong) NSDateFormatter *dateFormatter;
+
+@end
+
+static char intToHexChar(uint8_t i)
+{
+ return i >= 10 ? 'a' + i - 10 : '0' + i;
+}
+
+static char *SecHexCharFromBytes(const uint8_t *bytes, NSUInteger length, NSUInteger *outlen) {
+ // Fudge the math a bit on the assert because we don't want a 1GB string anyway
+ if (length > (NSUIntegerMax / 3)) {
+ return nil;
+ }
+ char *hex = calloc(1, length * 2 * 9 / 8); // 9/8 so we can inline ' ' between every 8 character sequence
+ char *destPtr = hex;
+
+ NSUInteger i;
+
+ for (i = 0; length > 4; i += 4, length -= 4) {
+ for (NSUInteger offset = 0; offset < 4; offset++) {
+ *destPtr++ = intToHexChar((bytes[i+offset] & 0xF0) >> 4);
+ *destPtr++ = intToHexChar(bytes[i+offset] & 0x0F);
+ }
+ *destPtr++ = ' ';
+ }
+
+ /* Using the same i from the above loop */
+ for (; length > 0; i++, length--) {
+ *destPtr++ = intToHexChar((bytes[i] & 0xF0) >> 4);
+ *destPtr++ = intToHexChar(bytes[i] & 0x0F);
+ }
+
+ if (outlen) *outlen = destPtr - hex;
+
+ return hex;
+}
+
+static BOOL SecCreateDirectoryAtPath(NSString *path, NSError **error) {
+ BOOL success = YES;
+ NSError *localError;
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+
+ if (![fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&localError]) {
+ if (![localError.domain isEqualToString:NSCocoaErrorDomain] || localError.code != NSFileWriteFileExistsError) {
+ success = NO;
+ }
+ }
+
+#if TARGET_OS_IPHONE
+ if (success) {
+ NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:&localError];
+ if (![attributes[NSFileProtectionKey] isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication]) {
+ [fileManager setAttributes:@{ NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication }
+ ofItemAtPath:path error:nil];
+ }
+ }
+#endif
+ if (!success) {
+ if (error) *error = localError;
+ }
+ return success;
+}
+
+@implementation NSData (CKUtilsAdditions)
+
+- (NSString *)CKHexString {
+ NSUInteger hexLen = 0;
+ NS_VALID_UNTIL_END_OF_SCOPE NSData *arcSafeSelf = self;
+ char *hex = SecHexCharFromBytes([arcSafeSelf bytes], [arcSafeSelf length], &hexLen);
+ return [[NSString alloc] initWithBytesNoCopy:hex length:hexLen encoding:NSASCIIStringEncoding freeWhenDone:YES];
+}
+
+- (NSString *)CKLowercaseHexStringWithoutSpaces {
+ NSMutableString *retVal = [[self CKHexString] mutableCopy];
+ [retVal replaceOccurrencesOfString:@" " withString:@"" options:0 range:NSMakeRange(0, [retVal length])];
+ return retVal;
+}
+
+- (NSString *)CKUppercaseHexStringWithoutSpaces {
+ NSMutableString *retVal = [[[self CKHexString] uppercaseString] mutableCopy];
+ [retVal replaceOccurrencesOfString:@" " withString:@"" options:0 range:NSMakeRange(0, [retVal length])];
+ return retVal;
+}
+
++ (NSData *)CKDataWithHexString:(NSString *)hexString stringIsUppercase:(BOOL)stringIsUppercase {
+ NSMutableData *retVal = [[NSMutableData alloc] init];
+ NSCharacterSet *hexCharacterSet = nil;
+ char aChar;
+ if (stringIsUppercase) {
+ hexCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"];
+ aChar = 'A';
+ } else {
+ hexCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdef"];
+ aChar = 'a';
+ }
+
+ unsigned int i;
+ for (i = 0; i < [hexString length] ; ) {
+ BOOL validFirstByte = NO;
+ BOOL validSecondByte = NO;
+ unichar firstByte = 0;
+ unichar secondByte = 0;
+
+ for ( ; i < [hexString length]; i++) {
+ firstByte = [hexString characterAtIndex:i];
+ if ([hexCharacterSet characterIsMember:firstByte]) {
+ i++;
+ validFirstByte = YES;
+ break;
+ }
+ }
+ for ( ; i < [hexString length]; i++) {
+ secondByte = [hexString characterAtIndex:i];
+ if ([hexCharacterSet characterIsMember:secondByte]) {
+ i++;
+ validSecondByte = YES;
+ break;
+ }
+ }
+ if (!validFirstByte || !validSecondByte) {
+ goto allDone;
+ }
+ if ((firstByte >= '0') && (firstByte <= '9')) {
+ firstByte -= '0';
+ } else {
+ firstByte = firstByte - aChar + 10;
+ }
+ if ((secondByte >= '0') && (secondByte <= '9')) {
+ secondByte -= '0';
+ } else {
+ secondByte = secondByte - aChar + 10;
+ }
+ char totalByteValue = (char)((firstByte << 4) + secondByte);
+
+ [retVal appendBytes:&totalByteValue length:1];
+ }
+allDone:
+ return retVal;
+}
+
++ (NSData *)CKDataWithHexString:(NSString *)hexString {
+ return [self CKDataWithHexString:hexString stringIsUppercase:NO];
+}
+
+@end
+
+@implementation SFSQLite
+
+@synthesize delegate = _delegate;
+@synthesize path = _path;
+@synthesize schema = _schema;
+@synthesize schemaVersion = _schemaVersion;
+@synthesize objectClassPrefix = _objectClassPrefix;
+@synthesize userVersion = _userVersion;
+@synthesize synchronousMode = _synchronousMode;
+@synthesize hasMigrated = _hasMigrated;
+@synthesize shouldVacuum = _shouldVacuum;
+@synthesize traced = _traced;
+@synthesize db = _db;
+@synthesize openCount = _openCount;
+@synthesize corrupt = _corrupt;
+@synthesize statementsBySQL = _statementsBySQL;
+@synthesize dateFormatter = _dateFormatter;
+#if DEBUG
+@synthesize unitTestOverrides = _unitTestOverrides;
+#endif
+
+- (instancetype)initWithPath:(NSString *)path schema:(NSString *)schema {
+ if ((self = [super init])) {
+ NSAssert([path length], @"Can't init a database with a zero-length path");
+ _path = path;
+ _schema = schema;
+ _schemaVersion = [self _createSchemaHash];
+ _statementsBySQL = [[NSMutableDictionary alloc] init];
+ _objectClassPrefix = @"CK";
+ _synchronousMode = SFSQLiteSynchronousModeNormal;
+ _hasMigrated = NO;
+ _shouldVacuum = YES;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ @autoreleasepool {
+ [self close];
+ }
+}
+
+- (SInt32)userVersion {
+ if (self.delegate) {
+ return self.delegate.userVersion;
+ }
+ return _userVersion;
+}
+
+- (NSString *)_synchronousModeString {
+ switch (self.synchronousMode) {
+ case SFSQLiteSynchronousModeOff:
+ return @"off";
+ case SFSQLiteSynchronousModeFull:
+ return @"full";
+ case SFSQLiteSynchronousModeNormal:
+ break;
+ default:
+ assert(0 && "Unknown synchronous mode");
+ }
+ return @"normal";
+}
+
+- (NSString *)_createSchemaHash {
+ unsigned char hashBuffer[CC_SHA256_DIGEST_LENGTH] = {0};
+ NSData *hashData = [NSData dataWithBytesNoCopy:hashBuffer length:CC_SHA256_DIGEST_LENGTH freeWhenDone:NO];
+ NS_VALID_UNTIL_END_OF_SCOPE NSData *schemaData = [self.schema dataUsingEncoding:NSUTF8StringEncoding];
+ CC_SHA256([schemaData bytes], (CC_LONG)[schemaData length], hashBuffer);
+ return [hashData CKUppercaseHexStringWithoutSpaces];
+}
+
+- (BOOL)isOpen {
+ return _db != NULL;
+}
+
+- (void)_periodicVacuum {
+ // "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.
+ // 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.
+ // In fact, because it moves pages around within the file, auto-vacuum can actually make fragmentation worse."
+ // https://sqlite.org/pragma.html#pragma_auto_vacuum
+ NSDate *lastVacuumDate = [NSDate dateWithTimeIntervalSinceReferenceDate:[[self propertyForKey:kSFSQLiteLastVacuumKey] floatValue]];
+ if ([lastVacuumDate timeIntervalSinceNow] < -(kCKSQLVacuumInterval)) {
+ [self executeSQL:@"VACUUM"];
+
+ NSString *vacuumDateString = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSinceReferenceDate]];
+ [self setProperty:vacuumDateString forKey:kSFSQLiteLastVacuumKey];
+ }
+}
+
+- (BOOL)openWithError:(NSError **)error {
+ BOOL success = NO;
+ NSError *localError;
+ NSString *dbSchemaVersion, *dir;
+ NSArray *results;
+ NS_VALID_UNTIL_END_OF_SCOPE NSString *arcSafePath = _path;
+
+ if (_openCount > 0) {
+ NSAssert(_db != NULL, @"Missing handle for open cache db");
+ _openCount += 1;
+ success = YES;
+ goto done;
+ }
+
+ // Create the directory for the cache.
+ dir = [_path stringByDeletingLastPathComponent];
+ if (!SecCreateDirectoryAtPath(dir, &localError)) {
+ goto done;
+ }
+
+ int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
+#if TARGET_OS_IPHONE
+ flags |= SQLITE_OPEN_FILEPROTECTION_COMPLETEUNTILFIRSTUSERAUTHENTICATION;
+#endif
+ int rc = sqlite3_open_v2([arcSafePath fileSystemRepresentation], &_db, flags, NULL);
+ if (rc != SQLITE_OK) {
+ localError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening db at %@, rc=%d(0x%x)", _path, rc, rc]}];
+ goto done;
+ }
+ sqlite3_extended_result_codes(_db, 1);
+ rc = sqlite3_busy_timeout(_db, kSFSQLiteBusyTimeout);
+ if (rc != SQLITE_OK) {
+ goto done;
+ }
+
+ // You don't argue with the Ben: rdar://12685305
+ [self executeSQL:@"pragma journal_mode = WAL"];
+ [self executeSQL:@"pragma synchronous = %@", [self _synchronousModeString]];
+ [self executeSQL:@"pragma auto_vacuum = FULL"];
+
+ // rdar://problem/32168789
+ // [self executeSQL:@"pragma foreign_keys = 1"];
+
+ // Initialize the db within a transaction in case there is a crash between creating the schema and setting the
+ // schema version, and to avoid multiple threads trying to re-create the db at once.
+ [self begin];
+
+ // Create the Properties table before trying to read the schema version from it. If the Properties table doesn't
+ // exist we can't prepare a statement to access it.
+ results = [self select:@[@"name"] from:@"sqlite_master" where:@"type = ? AND name = ?" bindings:@[@"table", @"Properties"]];
+ if (!results.count) {
+ [self executeSQL:kSFSQLiteCreatePropertiesTableSQL];
+ }
+
+ // Check the schema version and create or re-create the db if needed.
+ BOOL create = NO;
+ dbSchemaVersion = [self propertyForKey:kSFSQLiteSchemaVersionKey];
+ SInt32 dbUserVersion = [self dbUserVersion];
+
+ if (!dbSchemaVersion) {
+ // The schema version isn't set so the db was just created or we failed to initialize it previously.
+ create = YES;
+ } else if (![dbSchemaVersion isEqualToString:self.schemaVersion]
+ || (self.userVersion && dbUserVersion != self.userVersion)) {
+
+ if (self.delegate && [self.delegate migrateDatabase:self fromVersion:dbUserVersion]) {
+ _hasMigrated = YES;
+ }
+
+ if (!_hasMigrated) {
+ // 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.
+ [self removeAllStatements];
+ [self dropAllTables];
+ create = YES;
+ _hasMigrated = YES;
+ }
+ }
+ if (create) {
+ [self executeSQL:kSFSQLiteCreatePropertiesTableSQL];
+ [self executeSQL:@"%@", self.schema];
+ NSString *createdDateString = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSinceReferenceDate]];
+ [self setProperty:createdDateString forKey:kSFSQLiteCreatedDateKey];
+ }
+
+ [self end];
+
+#if DEBUG
+ // TODO: <rdar://problem/33115830> Resolve Race Condition When Setting 'userVersion/schemaVersion' in SFSQLite
+ if ([self.unitTestOverrides[@"RacyUserVersionUpdate"] isEqual:@YES]) {
+ success = YES;
+ goto done;
+ }
+#endif
+
+ if (self.shouldVacuum) [self _periodicVacuum];
+
+ if (create || _hasMigrated) {
+ [self setProperty:self.schemaVersion forKey:kSFSQLiteSchemaVersionKey];
+ if (self.userVersion) {
+ [self executeSQL:@"pragma user_version = %ld", (long)self.userVersion];
+ }
+ }
+
+ _openCount += 1;
+ success = YES;
+
+done:
+ if (!success && error) {
+ *error = localError;
+ }
+ return success;
+}
+
+- (void)open {
+ NSError *error;
+ if (![self openWithError:&error]) {
+ [self raise:@"Error opening db at %@: %@", self.path, error];
+ }
+}
+
+
+- (void)close {
+ if (_openCount > 0) {
+ if (_openCount == 1) {
+ NSAssert(_db != NULL, @"Missing handle for open cache db");
+
+ [self removeAllStatements];
+
+ if (sqlite3_close(_db)) {
+ [self raise:@"Error closing database"];
+ }
+ _db = NULL;
+ }
+ _openCount -= 1;
+ }
+}
+
+- (void)remove {
+ NSAssert(_openCount == 0, @"Trying to remove db at: %@ while it is open", _path);
+ [[NSFileManager defaultManager] removeItemAtPath:_path error:nil];
+ for (NSString *suffix in SFSQLiteJournalSuffixes()) {
+ [[NSFileManager defaultManager] removeItemAtPath:[_path stringByAppendingString:suffix] error:nil];
+ }
+}
+
+- (void)begin {
+ [self executeSQL:@"begin exclusive"];
+}
+
+- (void)end {
+ [self executeSQL:@"end"];
+}
+
+- (void)rollback {
+ [self executeSQL:@"rollback"];
+}
+
+- (void)analyze {
+ [self executeSQL:@"analyze"];
+}
+
+- (void)vacuum {
+ [self executeSQL:@"vacuum"];
+}
+
+- (void)raise:(NSString *)format, ... {
+ va_list args;
+ va_start(args, format);
+
+ NSString *reason = [[NSString alloc] initWithFormat:format arguments:args];
+
+ int code = 0;
+ int extendedCode = 0;
+ if (_db) {
+ code = sqlite3_errcode(_db) & 0xFF;
+ extendedCode = sqlite3_extended_errcode(_db);
+ const char *errmsg = sqlite3_errmsg(_db);
+
+ NSDictionary *dbAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:self.path error:NULL];
+ NSDictionary *fsAttrs = [[NSFileManager defaultManager] attributesOfFileSystemForPath:self.path error:NULL];
+ reason = [reason stringByAppendingFormat:@" - errcode:%04x, msg:\"%s\", size: %@, path:%@, fs:%@/%@", extendedCode, errmsg, dbAttrs[NSFileSize], _path, fsAttrs[NSFileSystemFreeSize], fsAttrs[NSFileSystemSize]];
+
+ if (!_corrupt && (code == SQLITE_CORRUPT || code == SQLITE_NOTADB)) {
+ _corrupt = YES;
+
+ @try {
+ [self close];
+ } @catch (NSException *x) {
+ NSLog(@"Warn: Error closing corrupt db: %@", x);
+ }
+
+ [self remove];
+ }
+ }
+
+ va_end(args);
+
+ [SFSQLiteError raise:reason code:code extended:extendedCode];
+}
+
+- (SFSQLiteRowID)lastInsertRowID {
+ if (!_db) {
+ [self raise:@"Database is closed"];
+ }
+
+ return sqlite3_last_insert_rowid(_db);
+}
+
+- (int)changes
+{
+ if (!_db) {
+ [self raise:@"Database is closed"];
+ }
+
+ return sqlite3_changes(_db);
+}
+
+- (void)executeSQL:(NSString *)format, ... {
+ va_list args;
+ va_start(args, format);
+ [self executeSQL:format arguments:args];
+ va_end(args);
+}
+
+- (void)executeSQL:(NSString *)format arguments:(va_list)args {
+ NS_VALID_UNTIL_END_OF_SCOPE NSString *SQL = [[NSString alloc] initWithFormat:format arguments:args];
+ if (!_db) {
+ [self raise:@"Database is closed"];
+ }
+ int execRet = sqlite3_exec(_db, [SQL UTF8String], NULL, NULL, NULL);
+ if (execRet != SQLITE_OK) {
+ [self raise:@"Error executing SQL: \"%@\" (%d)", SQL, execRet];
+ }
+}
+
+- (SFSQLiteStatement *)statementForSQL:(NSString *)SQL {
+ if (!_db) {
+ [self raise:@"Database is closed"];
+ }
+
+ SFSQLiteStatement *statement = _statementsBySQL[SQL];
+ if (statement) {
+ NSAssert(statement.isReset, @"Statement not reset after last use: \"%@\"", SQL);
+ } else {
+ sqlite3_stmt *handle = NULL;
+ NS_VALID_UNTIL_END_OF_SCOPE NSString *arcSafeSQL = SQL;
+ if (sqlite3_prepare_v2(_db, [arcSafeSQL UTF8String], -1, &handle, NULL)) {
+ [self raise:@"Error preparing statement: %@", SQL];
+ }
+
+ statement = [[SFSQLiteStatement alloc] initWithSQLite:self SQL:SQL handle:handle];
+ _statementsBySQL[SQL] = statement;
+ }
+
+ return statement;
+}
+
+- (void)removeAllStatements {
+ [[_statementsBySQL allValues] makeObjectsPerformSelector:@selector(finalizeStatement)];
+ [_statementsBySQL removeAllObjects];
+}
+
+- (NSArray *)allTableNames {
+ NSMutableArray *tableNames = [[NSMutableArray alloc] init];
+
+ SFSQLiteStatement *statement = [self statementForSQL:@"select name from sqlite_master where type = 'table'"];
+ while ([statement step]) {
+ NSString *name = [statement textAtIndex:0];
+ [tableNames addObject:name];
+ }
+ [statement reset];
+
+ return tableNames;
+}
+
+- (void)dropAllTables {
+ for (NSString *tableName in [self allTableNames]) {
+ [self executeSQL:@"drop table %@", tableName];
+ }
+}
+
+- (NSString *)propertyForKey:(NSString *)key {
+ NSAssert(key, @"Null key");
+
+ NSString *value = nil;
+
+ SFSQLiteStatement *statement = [self statementForSQL:@"select value from Properties where key = ?"];
+ [statement bindText:key atIndex:0];
+ if ([statement step]) {
+ value = [statement textAtIndex:0];
+ }
+ [statement reset];
+
+ return value;
+}
+
+- (void)setProperty:(NSString *)value forKey:(NSString *)key {
+ NSAssert(key, @"Null key");
+
+ if (value) {
+ SFSQLiteStatement *statement = [self statementForSQL:@"insert or replace into Properties (key, value) values (?,?)"];
+ [statement bindText:key atIndex:0];
+ [statement bindText:value atIndex:1];
+ [statement step];
+ [statement reset];
+ } else {
+ [self removePropertyForKey:key];
+ }
+}
+
+- (NSDateFormatter *)dateFormatter {
+ if (!_dateFormatter) {
+ NSDateFormatter* dateFormatter = [NSDateFormatter new];
+ dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZZZZZ";
+ _dateFormatter = dateFormatter;
+ }
+ return _dateFormatter;
+}
+
+- (NSDate *)datePropertyForKey:(NSString *)key {
+ NSString *dateStr = [self propertyForKey:key];
+ if (dateStr.length) {
+ return [self.dateFormatter dateFromString:dateStr];
+ }
+ return nil;
+}
+
+- (void)setDateProperty:(NSDate *)value forKey:(NSString *)key {
+ NSString *dateStr = nil;
+ if (value) {
+ dateStr = [self.dateFormatter stringFromDate:value];
+ }
+ [self setProperty:dateStr forKey:key];
+}
+
+- (void)removePropertyForKey:(NSString *)key {
+ NSAssert(key, @"Null key");
+
+ SFSQLiteStatement *statement = [self statementForSQL:@"delete from Properties where key = ?"];
+ [statement bindText:key atIndex:0];
+ [statement step];
+ [statement reset];
+}
+
+- (NSDate *)creationDate {
+ return [NSDate dateWithTimeIntervalSinceReferenceDate:[[self propertyForKey:kSFSQLiteCreatedDateKey] floatValue]];
+}
+
+// https://sqlite.org/pragma.html#pragma_table_info
+- (NSSet<NSString*> *)columnNamesForTable:(NSString*)tableName {
+ SFSQLiteStatement *statement = [self statementForSQL:[NSString stringWithFormat:@"pragma table_info(%@)", tableName]];
+ NSMutableSet<NSString*>* columnNames = [[NSMutableSet alloc] init];
+ while ([statement step]) {
+ [columnNames addObject:[statement textAtIndex:1]];
+ }
+ [statement reset];
+ return columnNames;
+}
+
+- (NSArray *)select:(NSArray *)columns from:(NSString *)tableName {
+ return [self select:columns from:tableName where:nil bindings:nil];
+}
+
+- (NSArray *)select:(NSArray *)columns from:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
+ NSMutableArray *results = [[NSMutableArray alloc] init];
+
+ NSMutableString *SQL = [NSMutableString stringWithFormat:@"select %@ from %@", [columns componentsJoinedByString:@", "], tableName];
+ if (whereSQL) {
+ [SQL appendFormat:@" where %@", whereSQL];
+ }
+
+ SFSQLiteStatement *statement = [self statementForSQL:SQL];
+ [statement bindValues:bindings];
+ while ([statement step]) {
+ [results addObject:[statement allObjectsByColumnName]];
+ }
+ [statement reset];
+
+ return results;
+}
+
+- (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 {
+ @autoreleasepool {
+ NSMutableString *SQL = [[NSMutableString alloc] init];
+ NSString *columnsString = @"*";
+ if ([columns count]) columnsString = [columns componentsJoinedByString:@", "];
+ [SQL appendFormat:@"select %@ from %@", columnsString, tableName];
+
+ if (whereSQL.length) {
+ [SQL appendFormat:@" where %@", whereSQL];
+ }
+ if (orderBy) {
+ NSString *orderByString = [orderBy componentsJoinedByString:@", "];
+ [SQL appendFormat:@" order by %@", orderByString];
+ }
+ if (limit) {
+ [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
+ }
+
+ SFSQLiteStatement *statement = [self statementForSQL:SQL];
+ [statement bindValues:bindings];
+ do {
+ @autoreleasepool {
+ if (![statement step]) {
+ break;
+ }
+ NSDictionary *stepResult = [statement allObjectsByColumnName];
+ if (block) {
+ BOOL stop = NO;
+ block(stepResult, &stop);
+ if (stop) {
+ break;
+ }
+ }
+ }
+ } while (1);
+ [statement reset];
+ }
+}
+
+- (void)selectFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings orderBy:(NSArray *)orderBy limit:(NSNumber *)limit block:(void (^)(NSDictionary *resultDictionary, BOOL *stop))block {
+ @autoreleasepool {
+ NSMutableString *SQL = [[NSMutableString alloc] init];
+ [SQL appendFormat:@"select * from %@", tableName];
+
+ if (whereSQL.length) {
+ [SQL appendFormat:@" where %@", whereSQL];
+ }
+ if (orderBy) {
+ NSString *orderByString = [orderBy componentsJoinedByString:@", "];
+ [SQL appendFormat:@" order by %@", orderByString];
+ }
+ if (limit) {
+ [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
+ }
+
+ SFSQLiteStatement *statement = [self statementForSQL:SQL];
+ [statement bindValues:bindings];
+ do {
+ @autoreleasepool {
+ if (![statement step]) {
+ break;
+ }
+ NSDictionary *stepResult = [statement allObjectsByColumnName];
+ if (block) {
+ BOOL stop = NO;
+ block(stepResult, &stop);
+ if (stop) {
+ break;
+ }
+ }
+ }
+ } while (1);
+ [statement reset];
+ }
+}
+
+- (NSArray *)selectFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings limit:(NSNumber *)limit {
+ NSMutableString *SQL = [[NSMutableString alloc] init];
+ [SQL appendFormat:@"select * from %@", tableName];
+
+ if (whereSQL.length) {
+ [SQL appendFormat:@" where %@", whereSQL];
+ }
+ if (limit) {
+ [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
+ }
+
+ NSMutableArray *results = [[NSMutableArray alloc] init];
+
+ SFSQLiteStatement *statement = [self statementForSQL:SQL];
+ [statement bindValues:bindings];
+ while ([statement step]) {
+ [results addObject:[statement allObjectsByColumnName]];
+ }
+ [statement reset];
+
+ return results;
+}
+
+- (void)update:(NSString *)tableName set:(NSString *)setSQL where:(NSString *)whereSQL bindings:(NSArray *)whereBindings limit:(NSNumber *)limit {
+ NSMutableString *SQL = [[NSMutableString alloc] init];
+ [SQL appendFormat:@"update %@", tableName];
+
+ NSAssert(setSQL.length > 0, @"null set expression");
+
+ [SQL appendFormat:@" set %@", setSQL];
+ if (whereSQL.length) {
+ [SQL appendFormat:@" where %@", whereSQL];
+ }
+ if (limit) {
+ [SQL appendFormat:@" limit %ld", (long)limit.integerValue];
+ }
+
+ SFSQLiteStatement *statement = [self statementForSQL:SQL];
+ [statement bindValues:whereBindings];
+ while ([statement step]) {
+ }
+ [statement reset];
+}
+
+- (NSArray *)selectAllFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
+ return [self selectFrom:tableName where:whereSQL bindings:bindings limit:nil];
+}
+
+- (NSUInteger)selectCountFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
+ NSArray *results = [self select:@[@"count(*) as n"] from:tableName where:whereSQL bindings:bindings];
+ return [results[0][@"n"] unsignedIntegerValue];
+}
+
+- (SFSQLiteRowID)insertOrReplaceInto:(NSString *)tableName values:(NSDictionary *)valuesByColumnName {
+ NSArray *columnNames = [[valuesByColumnName allKeys] sortedArrayUsingSelector:@selector(compare:)];
+ NSMutableArray *values = [[NSMutableArray alloc] init];
+ for (NSUInteger i = 0; i < columnNames.count; i++) {
+ values[i] = valuesByColumnName[columnNames[i]];
+ }
+
+ NSMutableString *SQL = [[NSMutableString alloc] initWithString:@"insert or replace into "];
+ [SQL appendString:tableName];
+ [SQL appendString:@" ("];
+ for (NSUInteger i = 0; i < columnNames.count; i++) {
+ [SQL appendString:columnNames[i]];
+ if (i != columnNames.count-1) {
+ [SQL appendString:@","];
+ }
+ }
+ [SQL appendString:@") values ("];
+ for (NSUInteger i = 0; i < columnNames.count; i++) {
+ if (i != columnNames.count-1) {
+ [SQL appendString:@"?,"];
+ } else {
+ [SQL appendString:@"?"];
+ }
+ }
+ [SQL appendString:@")"];
+
+ SFSQLiteStatement *statement = [self statementForSQL:SQL];
+ [statement bindValues:values];
+ [statement step];
+ [statement reset];
+
+ return [self lastInsertRowID];
+}
+
+- (void)deleteFrom:(NSString *)tableName matchingValues:(NSDictionary *)valuesByColumnName {
+ NSArray *columnNames = [[valuesByColumnName allKeys] sortedArrayUsingSelector:@selector(compare:)];
+ NSMutableArray *values = [[NSMutableArray alloc] init];
+ NSMutableString *whereSQL = [[NSMutableString alloc] init];
+ int bindingCount = 0;
+ for (NSUInteger i = 0; i < columnNames.count; i++) {
+ id value = valuesByColumnName[columnNames[i]];
+ [whereSQL appendString:columnNames[i]];
+ if (!value || [[NSNull null] isEqual:value]) {
+ [whereSQL appendString:@" is NULL"];
+ } else {
+ values[bindingCount++] = value;
+ [whereSQL appendString:@"=?"];
+ }
+ if (i != columnNames.count-1) {
+ [whereSQL appendString:@" AND "];
+ }
+ }
+ [self deleteFrom:tableName where:whereSQL bindings:values];
+}
+
+- (void)deleteFrom:(NSString *)tableName where:(NSString *)whereSQL bindings:(NSArray *)bindings {
+ NSString *SQL = [NSString stringWithFormat:@"delete from %@ where %@", tableName, whereSQL];
+
+ SFSQLiteStatement *statement = [self statementForSQL:SQL];
+ [statement bindValues:bindings];
+ [statement step];
+ [statement reset];
+}
+
+- (NSString *)_tableNameForClass:(Class)objectClass {
+ NSString *className = [objectClass SFSQLiteClassName];
+ if (![className hasPrefix:_objectClassPrefix]) {
+ [NSException raise:NSInvalidArgumentException format:@"Object class \"%@\" does not have prefix \"%@\"", className, _objectClassPrefix];
+ }
+ return [className substringFromIndex:_objectClassPrefix.length];
+}
+
+- (SInt32)dbUserVersion {
+ SInt32 userVersion = 0;
+ SFSQLiteStatement *statement = [self statementForSQL:@"pragma user_version"];
+ while ([statement step]) {
+ userVersion = [statement intAtIndex:0];
+ }
+ [statement reset];
+
+ return userVersion;
+}
+
+@end
+
+
+#define SFSQLiteErrorRaiseMethod(SQLiteError) + (void)SQLiteError:(NSString *)reason { [NSException raise:NSGenericException format:@"%@", reason]; }
+#define SFSQLiteErrorCase(SQLiteError) case SQLITE_ ## SQLiteError: [self SQLiteError:reason]; break
+
+@implementation SFSQLiteError
+
+// SQLite error codes
+SFSQLiteErrorRaiseMethod(ERROR)
+SFSQLiteErrorRaiseMethod(INTERNAL)
+SFSQLiteErrorRaiseMethod(PERM)
+SFSQLiteErrorRaiseMethod(ABORT)
+SFSQLiteErrorRaiseMethod(BUSY)
+SFSQLiteErrorRaiseMethod(LOCKED)
+SFSQLiteErrorRaiseMethod(NOMEM)
+SFSQLiteErrorRaiseMethod(READONLY)
+SFSQLiteErrorRaiseMethod(INTERRUPT)
+SFSQLiteErrorRaiseMethod(IOERR)
+SFSQLiteErrorRaiseMethod(CORRUPT)
+SFSQLiteErrorRaiseMethod(NOTFOUND)
+SFSQLiteErrorRaiseMethod(FULL)
+SFSQLiteErrorRaiseMethod(CANTOPEN)
+SFSQLiteErrorRaiseMethod(PROTOCOL)
+SFSQLiteErrorRaiseMethod(SCHEMA)
+SFSQLiteErrorRaiseMethod(TOOBIG)
+SFSQLiteErrorRaiseMethod(CONSTRAINT)
+SFSQLiteErrorRaiseMethod(MISMATCH)
+SFSQLiteErrorRaiseMethod(MISUSE)
+SFSQLiteErrorRaiseMethod(RANGE)
+SFSQLiteErrorRaiseMethod(NOTADB)
+
+// SQLite extended error codes
+SFSQLiteErrorRaiseMethod(IOERR_READ)
+SFSQLiteErrorRaiseMethod(IOERR_SHORT_READ)
+SFSQLiteErrorRaiseMethod(IOERR_WRITE)
+SFSQLiteErrorRaiseMethod(IOERR_FSYNC)
+SFSQLiteErrorRaiseMethod(IOERR_DIR_FSYNC)
+SFSQLiteErrorRaiseMethod(IOERR_TRUNCATE)
+SFSQLiteErrorRaiseMethod(IOERR_FSTAT)
+SFSQLiteErrorRaiseMethod(IOERR_UNLOCK)
+SFSQLiteErrorRaiseMethod(IOERR_RDLOCK)
+SFSQLiteErrorRaiseMethod(IOERR_DELETE)
+SFSQLiteErrorRaiseMethod(IOERR_BLOCKED)
+SFSQLiteErrorRaiseMethod(IOERR_NOMEM)
+SFSQLiteErrorRaiseMethod(IOERR_ACCESS)
+SFSQLiteErrorRaiseMethod(IOERR_CHECKRESERVEDLOCK)
+SFSQLiteErrorRaiseMethod(IOERR_LOCK)
+SFSQLiteErrorRaiseMethod(IOERR_CLOSE)
+SFSQLiteErrorRaiseMethod(IOERR_DIR_CLOSE)
+SFSQLiteErrorRaiseMethod(IOERR_SHMOPEN)
+SFSQLiteErrorRaiseMethod(IOERR_SHMSIZE)
+SFSQLiteErrorRaiseMethod(IOERR_SHMLOCK)
+SFSQLiteErrorRaiseMethod(IOERR_SHMMAP)
+SFSQLiteErrorRaiseMethod(IOERR_SEEK)
+SFSQLiteErrorRaiseMethod(IOERR_DELETE_NOENT)
+SFSQLiteErrorRaiseMethod(IOERR_MMAP)
+SFSQLiteErrorRaiseMethod(IOERR_GETTEMPPATH)
+SFSQLiteErrorRaiseMethod(IOERR_CONVPATH)
+SFSQLiteErrorRaiseMethod(LOCKED_SHAREDCACHE)
+SFSQLiteErrorRaiseMethod(BUSY_RECOVERY)
+SFSQLiteErrorRaiseMethod(BUSY_SNAPSHOT)
+SFSQLiteErrorRaiseMethod(CANTOPEN_NOTEMPDIR)
+SFSQLiteErrorRaiseMethod(CANTOPEN_ISDIR)
+SFSQLiteErrorRaiseMethod(CANTOPEN_FULLPATH)
+SFSQLiteErrorRaiseMethod(CANTOPEN_CONVPATH)
+SFSQLiteErrorRaiseMethod(CORRUPT_VTAB)
+SFSQLiteErrorRaiseMethod(READONLY_RECOVERY)
+SFSQLiteErrorRaiseMethod(READONLY_CANTLOCK)
+SFSQLiteErrorRaiseMethod(READONLY_ROLLBACK)
+SFSQLiteErrorRaiseMethod(READONLY_DBMOVED)
+SFSQLiteErrorRaiseMethod(ABORT_ROLLBACK)
+SFSQLiteErrorRaiseMethod(CONSTRAINT_CHECK)
+SFSQLiteErrorRaiseMethod(CONSTRAINT_COMMITHOOK)
+SFSQLiteErrorRaiseMethod(CONSTRAINT_FOREIGNKEY)
+SFSQLiteErrorRaiseMethod(CONSTRAINT_FUNCTION)
+SFSQLiteErrorRaiseMethod(CONSTRAINT_NOTNULL)
+SFSQLiteErrorRaiseMethod(CONSTRAINT_PRIMARYKEY)
+SFSQLiteErrorRaiseMethod(CONSTRAINT_TRIGGER)
+SFSQLiteErrorRaiseMethod(CONSTRAINT_UNIQUE)
+SFSQLiteErrorRaiseMethod(CONSTRAINT_VTAB)
+SFSQLiteErrorRaiseMethod(CONSTRAINT_ROWID)
+SFSQLiteErrorRaiseMethod(NOTICE_RECOVER_WAL)
+SFSQLiteErrorRaiseMethod(NOTICE_RECOVER_ROLLBACK)
+
++ (void)raise:(NSString *)reason code:(int)code extended:(int)extended {
+ switch(extended) {
+ SFSQLiteErrorCase(IOERR_READ);
+ SFSQLiteErrorCase(IOERR_SHORT_READ);
+ SFSQLiteErrorCase(IOERR_WRITE);
+ SFSQLiteErrorCase(IOERR_FSYNC);
+ SFSQLiteErrorCase(IOERR_DIR_FSYNC);
+ SFSQLiteErrorCase(IOERR_TRUNCATE);
+ SFSQLiteErrorCase(IOERR_FSTAT);
+ SFSQLiteErrorCase(IOERR_UNLOCK);
+ SFSQLiteErrorCase(IOERR_RDLOCK);
+ SFSQLiteErrorCase(IOERR_DELETE);
+ SFSQLiteErrorCase(IOERR_BLOCKED);
+ SFSQLiteErrorCase(IOERR_NOMEM);
+ SFSQLiteErrorCase(IOERR_ACCESS);
+ SFSQLiteErrorCase(IOERR_CHECKRESERVEDLOCK);
+ SFSQLiteErrorCase(IOERR_LOCK);
+ SFSQLiteErrorCase(IOERR_CLOSE);
+ SFSQLiteErrorCase(IOERR_DIR_CLOSE);
+ SFSQLiteErrorCase(IOERR_SHMOPEN);
+ SFSQLiteErrorCase(IOERR_SHMSIZE);
+ SFSQLiteErrorCase(IOERR_SHMLOCK);
+ SFSQLiteErrorCase(IOERR_SHMMAP);
+ SFSQLiteErrorCase(IOERR_SEEK);
+ SFSQLiteErrorCase(IOERR_DELETE_NOENT);
+ SFSQLiteErrorCase(IOERR_MMAP);
+ SFSQLiteErrorCase(IOERR_GETTEMPPATH);
+ SFSQLiteErrorCase(IOERR_CONVPATH);
+ SFSQLiteErrorCase(LOCKED_SHAREDCACHE);
+ SFSQLiteErrorCase(BUSY_RECOVERY);
+ SFSQLiteErrorCase(BUSY_SNAPSHOT);
+ SFSQLiteErrorCase(CANTOPEN_NOTEMPDIR);
+ SFSQLiteErrorCase(CANTOPEN_ISDIR);
+ SFSQLiteErrorCase(CANTOPEN_FULLPATH);
+ SFSQLiteErrorCase(CANTOPEN_CONVPATH);
+ SFSQLiteErrorCase(CORRUPT_VTAB);
+ SFSQLiteErrorCase(READONLY_RECOVERY);
+ SFSQLiteErrorCase(READONLY_CANTLOCK);
+ SFSQLiteErrorCase(READONLY_ROLLBACK);
+ SFSQLiteErrorCase(READONLY_DBMOVED);
+ SFSQLiteErrorCase(ABORT_ROLLBACK);
+ SFSQLiteErrorCase(CONSTRAINT_CHECK);
+ SFSQLiteErrorCase(CONSTRAINT_COMMITHOOK);
+ SFSQLiteErrorCase(CONSTRAINT_FOREIGNKEY);
+ SFSQLiteErrorCase(CONSTRAINT_FUNCTION);
+ SFSQLiteErrorCase(CONSTRAINT_NOTNULL);
+ SFSQLiteErrorCase(CONSTRAINT_PRIMARYKEY);
+ SFSQLiteErrorCase(CONSTRAINT_TRIGGER);
+ SFSQLiteErrorCase(CONSTRAINT_UNIQUE);
+ SFSQLiteErrorCase(CONSTRAINT_VTAB);
+ SFSQLiteErrorCase(CONSTRAINT_ROWID);
+ SFSQLiteErrorCase(NOTICE_RECOVER_WAL);
+ SFSQLiteErrorCase(NOTICE_RECOVER_ROLLBACK);
+ default: break;
+ }
+ switch(code) {
+ SFSQLiteErrorCase(ERROR);
+ SFSQLiteErrorCase(INTERNAL);
+ SFSQLiteErrorCase(PERM);
+ SFSQLiteErrorCase(ABORT);
+ SFSQLiteErrorCase(BUSY);
+ SFSQLiteErrorCase(LOCKED);
+ SFSQLiteErrorCase(NOMEM);
+ SFSQLiteErrorCase(READONLY);
+ SFSQLiteErrorCase(INTERRUPT);
+ SFSQLiteErrorCase(IOERR);
+ SFSQLiteErrorCase(CORRUPT);
+ SFSQLiteErrorCase(NOTFOUND);
+ SFSQLiteErrorCase(FULL);
+ SFSQLiteErrorCase(CANTOPEN);
+ SFSQLiteErrorCase(PROTOCOL);
+ SFSQLiteErrorCase(SCHEMA);
+ SFSQLiteErrorCase(TOOBIG);
+ SFSQLiteErrorCase(CONSTRAINT);
+ SFSQLiteErrorCase(MISMATCH);
+ SFSQLiteErrorCase(MISUSE);
+ SFSQLiteErrorCase(RANGE);
+ SFSQLiteErrorCase(NOTADB);
+ default: break;
+ }
+ [NSException raise:NSGenericException format:@"%@", reason];
+}
+
+@end