diff --git a/Signal/test/util/OWSDatabaseConverterTest.m b/Signal/test/util/OWSDatabaseConverterTest.m index 5c7014ebf..5b4b74c01 100644 --- a/Signal/test/util/OWSDatabaseConverterTest.m +++ b/Signal/test/util/OWSDatabaseConverterTest.m @@ -6,7 +6,9 @@ #import "OWSDatabaseConverter.h" #import #import +#import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -33,37 +35,122 @@ NS_ASSUME_NONNULL_BEGIN return [Randomness generateRandomBytes:30]; } -- (nullable NSString *)createUnconvertedDatabase:(NSData *)passwordData +- (void)openYapDatabase:(NSString *)databaseFilePath + databasePassword:(NSData *)databasePassword + databaseBlock:(void (^_Nonnull)(YapDatabase *))databaseBlock +{ + OWSAssert(databaseFilePath.length > 0); + OWSAssert(databasePassword.length > 0); + OWSAssert(databaseBlock); + + DDLogVerbose(@"openYapDatabase: %@", databaseFilePath); + + __weak YapDatabase *_Nullable weakDatabase = nil; + dispatch_queue_t snapshotQueue; + dispatch_queue_t writeQueue; + + @autoreleasepool { + YapDatabaseOptions *options = [[YapDatabaseOptions alloc] init]; + options.corruptAction = YapDatabaseCorruptAction_Fail; + options.cipherKeyBlock = ^{ + return databasePassword; + }; + options.enableMultiProcessSupport = YES; + + OWSAssert(options.cipherDefaultkdfIterNumber == 0); + OWSAssert(options.kdfIterNumber == 0); + OWSAssert(options.cipherPageSize == 0); + OWSAssert(options.pragmaPageSize == 0); + OWSAssert(options.pragmaJournalSizeLimit == 0); + + YapDatabase *database = [[YapDatabase alloc] initWithPath:databaseFilePath + serializer:nil + deserializer:[OWSStorage logOnFailureDeserializer] + options:options]; + OWSAssert(database); + + weakDatabase = database; + snapshotQueue = database->snapshotQueue; + writeQueue = database->writeQueue; + + databaseBlock(database); + + // Close the database. + database = nil; + } + + // Flush the database's queues, which may contain lingering + // references to the database. + dispatch_sync(snapshotQueue, + ^{ + }); + dispatch_sync(writeQueue, + ^{ + }); + + // Wait for notifications from writes to be fired. + { + XCTestExpectation *expectation = [self expectationWithDescription:@"Database modified notifications"]; + + dispatch_async(dispatch_get_main_queue(), ^{ + // Database modified notifications are fired on the main queue. + // Once this block executes, the main queue has been flushed + // and we know that all database modified notifications are + // complete. + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:5.0 + handler:^(NSError *error) { + if (error) { + NSLog(@"Timeout Error: %@", error); + } + }]; + } + + YapDatabase *_Nullable strongDatabase = weakDatabase; + OWSAssert(!strongDatabase); +} + +- (nullable NSString *)createUnconvertedDatabase:(NSData *)databasePassword { NSString *temporaryDirectory = NSTemporaryDirectory(); NSString *filename = [NSUUID UUID].UUIDString; NSString *databaseFilePath = [temporaryDirectory stringByAppendingPathComponent:filename]; - YapDatabaseOptions *options = [[YapDatabaseOptions alloc] init]; - options.corruptAction = YapDatabaseCorruptAction_Fail; - options.cipherKeyBlock = ^{ - return passwordData; - }; - options.enableMultiProcessSupport = YES; - - OWSAssert(options.cipherDefaultkdfIterNumber == 0); - OWSAssert(options.kdfIterNumber == 0); - OWSAssert(options.cipherPageSize == 0); - OWSAssert(options.pragmaPageSize == 0); - OWSAssert(options.pragmaJournalSizeLimit == 0); - - YapDatabase *database = [[YapDatabase alloc] initWithPath:databaseFilePath - serializer:nil - deserializer:[OWSStorage logOnFailureDeserializer] - options:options]; - OWSAssert(database); - return database ? databaseFilePath : nil; + [self openYapDatabase:databaseFilePath + databasePassword:databasePassword + databaseBlock:^(YapDatabase *database) { + YapDatabaseConnection *dbConnection = database.newConnection; + [dbConnection setObject:@(YES) forKey:@"test_key_name" inCollection:@"test_collection_name"]; + [dbConnection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() completionBlock:nil]; + }]; + + OWSAssert([[NSFileManager defaultManager] fileExistsAtPath:databaseFilePath]); + + [self openYapDatabase:databaseFilePath + databasePassword:databasePassword + databaseBlock:^(YapDatabase *database) { + YapDatabaseConnection *dbConnection = database.newConnection; + id _Nullable value = [dbConnection objectForKey:@"test_key_name" inCollection:@"test_collection_name"]; + OWSAssert([@(YES) isEqual:value]); + }]; + + OWSAssert([[NSFileManager defaultManager] fileExistsAtPath:databaseFilePath]); + + NSError *_Nullable error = nil; + NSDictionary *fileAttributes = + [[NSFileManager defaultManager] attributesOfItemAtPath:databaseFilePath error:&error]; + OWSAssert(fileAttributes && !error); + DDLogVerbose(@"%@ test database file size: %@", self.logTag, fileAttributes[NSFileSize]); + + return databaseFilePath; } - (void)testDoesDatabaseNeedToBeConverted_Unconverted { - NSData *passwordData = [self randomDatabasePassword]; - NSString *_Nullable databaseFilePath = [self createUnconvertedDatabase:passwordData]; + NSData *databasePassword = [self randomDatabasePassword]; + NSString *_Nullable databaseFilePath = [self createUnconvertedDatabase:databasePassword]; XCTAssertTrue([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); } @@ -74,10 +161,15 @@ NS_ASSUME_NONNULL_BEGIN - (void)testDatabaseConversion { - NSData *passwordData = [self randomDatabasePassword]; - NSString *_Nullable databaseFilePath = [self createUnconvertedDatabase:passwordData]; + NSData *databasePassword = [self randomDatabasePassword]; + NSString *_Nullable databaseFilePath = [self createUnconvertedDatabase:databasePassword]; XCTAssertTrue([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); - [OWSDatabaseConverter convertDatabaseIfNecessary:databaseFilePath]; + NSError *_Nullable error = + [OWSDatabaseConverter convertDatabaseIfNecessary:databaseFilePath databasePassword:databasePassword]; + if (error) { + DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error); + } + XCTAssertNil(error); XCTAssertFalse([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); } diff --git a/SignalMessaging/utils/OWSDatabaseConverter.h b/SignalMessaging/utils/OWSDatabaseConverter.h index cafbc740c..92774b23c 100644 --- a/SignalMessaging/utils/OWSDatabaseConverter.h +++ b/SignalMessaging/utils/OWSDatabaseConverter.h @@ -12,8 +12,9 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; -+ (void)convertDatabaseIfNecessary; -+ (void)convertDatabaseIfNecessary:(NSString *)databaseFilePath; ++ (nullable NSError *)convertDatabaseIfNecessary; ++ (nullable NSError *)convertDatabaseIfNecessary:(NSString *)databaseFilePath + databasePassword:(NSData *)databasePassword; @end diff --git a/SignalMessaging/utils/OWSDatabaseConverter.m b/SignalMessaging/utils/OWSDatabaseConverter.m index d5e7e57f3..9bd9c7410 100644 --- a/SignalMessaging/utils/OWSDatabaseConverter.m +++ b/SignalMessaging/utils/OWSDatabaseConverter.m @@ -4,42 +4,64 @@ #import "OWSDatabaseConverter.h" #import "sqlite3.h" +#import #import #import #import NS_ASSUME_NONNULL_BEGIN -const int kSqliteHeaderLength = 32; +const NSUInteger kSqliteHeaderLength = 32; + +@interface OWSStorage (OWSDatabaseConverter) + ++ (YapDatabaseDeserializer)logOnFailureDeserializer; + +@end + +#pragma mark - @implementation OWSDatabaseConverter ++ (NSData *)readFirstNBytesOfDatabaseFile:(NSString *)filePath byteCount:(NSUInteger)byteCount +{ + OWSAssert(filePath.length > 0); + + @autoreleasepool { + NSError *error; + // We use NSDataReadingMappedAlways instead of NSDataReadingMappedIfSafe because + // we know the database will always exist for the duration of this instance of NSData. + NSData *_Nullable data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:filePath] + options:NSDataReadingMappedAlways + error:&error]; + if (!data || error) { + DDLogError(@"%@ Couldn't read database file header.", self.logTag); + // TODO: Make a convenience method (on a category of NSException?) that + // flushes DDLog before raising a terminal exception. + [NSException raise:@"Couldn't read database file header" format:@""]; + } + // Pull this constant out so that we can use it in our YapDatabase fork. + NSData *_Nullable headerData = [data subdataWithRange:NSMakeRange(0, byteCount)]; + if (!headerData || headerData.length != byteCount) { + [NSException raise:@"Database file header has unexpected length" + format:@"Database file header has unexpected length: %zd", headerData.length]; + } + return [headerData copy]; + } +} + + (BOOL)doesDatabaseNeedToBeConverted:(NSString *)databaseFilePath { OWSAssert(databaseFilePath.length > 0); if (![[NSFileManager defaultManager] fileExistsAtPath:databaseFilePath]) { - DDLogVerbose(@"%@ Skipping database conversion; no legacy database found.", self.logTag); - return NO; - } - NSError *error; - // We use NSDataReadingMappedAlways instead of NSDataReadingMappedIfSafe because - // we know the database will always exist for the duration of this instance of NSData. - NSData *_Nullable data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:databaseFilePath] - options:NSDataReadingMappedAlways - error:&error]; - if (!data || error) { - DDLogError(@"%@ Couldn't read legacy database file header.", self.logTag); - // TODO: Make a convenience method (on a category of NSException?) that - // flushes DDLog before raising a terminal exception. - [NSException raise:@"Couldn't read legacy database file header" format:@""]; - } - // Pull this constant out so that we can use it in our YapDatabase fork. - NSData *_Nullable headerData = [data subdataWithRange:NSMakeRange(0, kSqliteHeaderLength)]; - if (!headerData || headerData.length != kSqliteHeaderLength) { - [NSException raise:@"Database database file header has unexpected length" - format:@"Database database file header has unexpected length: %zd", headerData.length]; + DDLogVerbose(@"%@ database file not found.", self.logTag); + return nil; } + + NSData *headerData = [self readFirstNBytesOfDatabaseFile:databaseFilePath byteCount:kSqliteHeaderLength]; + OWSAssert(headerData); + NSString *kUnencryptedHeader = @"SQLite format 3\0"; NSData *unencryptedHeaderData = [kUnencryptedHeader dataUsingEncoding:NSUTF8StringEncoding]; BOOL isUnencrypted = [unencryptedHeaderData @@ -52,36 +74,46 @@ const int kSqliteHeaderLength = 32; return YES; } -+ (void)convertDatabaseIfNecessary ++ (nullable NSError *)convertDatabaseIfNecessary { NSString *databaseFilePath = [TSStorageManager legacyDatabaseFilePath]; - [self convertDatabaseIfNecessary:databaseFilePath]; + + NSError *error; + NSData *_Nullable databasePassword = [OWSStorage tryToLoadDatabasePassword:&error]; + if (!databasePassword || error) { + return (error + ?: OWSErrorWithCodeDescription( + OWSErrorCodeDatabaseConversionFatalError, @"Failed to load database password")); + } + + return [self convertDatabaseIfNecessary:databaseFilePath databasePassword:databasePassword]; } // TODO upon failure show user error UI // TODO upon failure anything we need to do "back out" partial migration -+ (void)convertDatabaseIfNecessary:(NSString *)databaseFilePath ++ (nullable NSError *)convertDatabaseIfNecessary:(NSString *)databaseFilePath + databasePassword:(NSData *)databasePassword { if (![self doesDatabaseNeedToBeConverted:databaseFilePath]) { - return; + return nil; } - [self convertDatabase:(NSString *)databaseFilePath]; + return [self convertDatabase:(NSString *)databaseFilePath databasePassword:databasePassword]; } -+ (nullable NSError *)convertDatabase:(NSString *)databaseFilePath ++ (nullable NSError *)convertDatabase:(NSString *)databaseFilePath databasePassword:(NSData *)databasePassword { OWSAssert(databaseFilePath.length > 0); + OWSAssert(databasePassword.length > 0); - NSError *error; - NSData *_Nullable databasePassword = [OWSStorage tryToLoadDatabasePassword:&error]; - if (!databasePassword || error) { - return (error - ?: OWSErrorWithCodeDescription( - OWSErrorCodeDatabaseConversionFatalError, @"Failed to load database password")); - } + NSData *headerData = [self readFirstNBytesOfDatabaseFile:databaseFilePath byteCount:kSqliteHeaderLength]; + OWSAssert(headerData); + + const NSUInteger kSQLCipherSaltLength = 16; + OWSAssert(headerData.length >= kSQLCipherSaltLength); + NSData *sqlCipherSaltData = [headerData subdataWithRange:NSMakeRange(0, kSQLCipherSaltLength)]; - // TODO: + // TODO: Write salt to keychain. // Hello Matthew, // @@ -181,139 +213,156 @@ const int kSqliteHeaderLength = 32; return OWSErrorWithCodeDescription(OWSErrorCodeDatabaseConversionFatalError, @"Failed to set SQLCipher key"); } - // TODO set plaintext pragma - // TODO modify first page - // TODO force checkpoint - // ----------------------------------------------------------- // // This block was derived from [Yapdatabase configureDatabase]. - // - // { - // int status; - // - // // Set mandatory pragmas - // + { + // + // { + // int status; + // + // // Set mandatory pragmas + // + + // MJK: this isn't relevant since we only migrate existing databses and never set a pageSize option. + // if (isNewDatabaseFile && (options.pragmaPageSize > 0)) + // { + // NSString *pragma_page_size = + // [NSString stringWithFormat:@"PRAGMA page_size = %ld;", (long)options.pragmaPageSize]; + // + // status = sqlite3_exec(db, [pragma_page_size UTF8String], NULL, NULL, NULL); + // if (status != SQLITE_OK) + // { + // YDBLogError(@"Error setting PRAGMA page_size: %d %s", status, sqlite3_errmsg(db)); + // } + // } + + // + status = sqlite3_exec(db, "PRAGMA journal_mode = WAL;", NULL, NULL, NULL); + if (status != SQLITE_OK) { + DDLogError(@"Error setting PRAGMA journal_mode: %d %s", status, sqlite3_errmsg(db)); + return OWSErrorWithCodeDescription(OWSErrorCodeDatabaseConversionFatalError, @"Failed to set WAL mode"); + } - // MJK: this isn't relevant since we only migrate existing databses and never set a pageSize option. - // if (isNewDatabaseFile && (options.pragmaPageSize > 0)) - // { - // NSString *pragma_page_size = - // [NSString stringWithFormat:@"PRAGMA page_size = %ld;", (long)options.pragmaPageSize]; - // - // status = sqlite3_exec(db, [pragma_page_size UTF8String], NULL, NULL, NULL); - // if (status != SQLITE_OK) - // { - // YDBLogError(@"Error setting PRAGMA page_size: %d %s", status, sqlite3_errmsg(db)); - // } - // } + // MJK: this isn't relevant since we only migrate existing databses + // if (isNewDatabaseFile) + // { + // status = sqlite3_exec(db, "PRAGMA auto_vacuum = FULL; VACUUM;", NULL, NULL, NULL); + // if (status != SQLITE_OK) + // { + // YDBLogError(@"Error setting PRAGMA auto_vacuum: %d %s", status, sqlite3_errmsg(db)); + // } + // } + // + + // TODO verify we need to do this. + // Set synchronous to normal for THIS sqlite instance. + // + // This does NOT affect normal connections. + // That is, this does NOT affect YapDatabaseConnection instances. + // The sqlite connections of normal YapDatabaseConnection instances will follow the set pragmaSynchronous value. + // + // The reason we hardcode normal for this sqlite instance is because + // it's only used to write the initial snapshot value. + // And this doesn't need to be durable, as it is initialized to zero everytime. + // + // (This sqlite db is also used to perform checkpoints. + // But a normal value won't affect these operations, + // as they will perform sync operations whether the connection is normal or full.) + status = sqlite3_exec(db, "PRAGMA synchronous = NORMAL;", NULL, NULL, NULL); + if (status != SQLITE_OK) { + DDLogError(@"Error setting PRAGMA synchronous: %d %s", status, sqlite3_errmsg(db)); + // This isn't critical, so we can continue. + } - // - status = sqlite3_exec(db, "PRAGMA journal_mode = WAL;", NULL, NULL, NULL); - if (status != SQLITE_OK) { - DDLogError(@"Error setting PRAGMA journal_mode: %d %s", status, sqlite3_errmsg(db)); - return OWSErrorWithCodeDescription(OWSErrorCodeDatabaseConversionFatalError, @"Failed to set WAL mode"); - } + // Set journal_size_imit. + // + // We only need to do set this pragma for THIS connection, + // because it is the only connection that performs checkpoints. - // MJK: this isn't relevant since we only migrate existing databses - // if (isNewDatabaseFile) - // { - // status = sqlite3_exec(db, "PRAGMA auto_vacuum = FULL; VACUUM;", NULL, NULL, NULL); - // if (status != SQLITE_OK) - // { - // YDBLogError(@"Error setting PRAGMA auto_vacuum: %d %s", status, sqlite3_errmsg(db)); - // } - // } - // + NSInteger defaultPragmaJournalSizeLimit = 0; + NSString *pragma_journal_size_limit = + [NSString stringWithFormat:@"PRAGMA journal_size_limit = %ld;", (long)defaultPragmaJournalSizeLimit]; - // TODO verify we need to do this. - // Set synchronous to normal for THIS sqlite instance. - // - // This does NOT affect normal connections. - // That is, this does NOT affect YapDatabaseConnection instances. - // The sqlite connections of normal YapDatabaseConnection instances will follow the set pragmaSynchronous value. - // - // The reason we hardcode normal for this sqlite instance is because - // it's only used to write the initial snapshot value. - // And this doesn't need to be durable, as it is initialized to zero everytime. - // - // (This sqlite db is also used to perform checkpoints. - // But a normal value won't affect these operations, - // as they will perform sync operations whether the connection is normal or full.) - status = sqlite3_exec(db, "PRAGMA synchronous = NORMAL;", NULL, NULL, NULL); - if (status != SQLITE_OK) { - DDLogError(@"Error setting PRAGMA synchronous: %d %s", status, sqlite3_errmsg(db)); - // This isn't critical, so we can continue. + status = sqlite3_exec(db, [pragma_journal_size_limit UTF8String], NULL, NULL, NULL); + if (status != SQLITE_OK) { + DDLogError(@"Error setting PRAGMA journal_size_limit: %d %s", status, sqlite3_errmsg(db)); + // This isn't critical, so we can continue. + } + // + // // Set mmap_size (if needed). + // // + // // This configures memory mapped I/O. + // // OWS: we currently don't set options.pragmaMMapSize, so we can ignore this code. + // if (options.pragmaMMapSize > 0) + // { + // NSString *pragma_mmap_size = + // [NSString stringWithFormat:@"PRAGMA mmap_size = %ld;", (long)options.pragmaMMapSize]; + // + // status = sqlite3_exec(db, [pragma_mmap_size UTF8String], NULL, NULL, NULL); + // if (status != SQLITE_OK) + // { + // YDBLogError(@"Error setting PRAGMA mmap_size: %d %s", status, sqlite3_errmsg(db)); + // // This isn't critical, so we can continue. + // } + // } + // + // Disable autocheckpointing. + // + // YapDatabase has its own optimized checkpointing algorithm built-in. + // It knows the state of every active connection for the database, + // so it can invoke the checkpoint methods at the precise time in which a checkpoint can be most effective. + sqlite3_wal_autocheckpoint(db, 0); + + // END DB setup copied from YapDatabase + // BEGIN SQLCipher migration } - // Set journal_size_imit. - // - // We only need to do set this pragma for THIS connection, - // because it is the only connection that performs checkpoints. - - NSInteger defaultPragmaJournalSizeLimit = 0; - NSString *pragma_journal_size_limit = - [NSString stringWithFormat:@"PRAGMA journal_size_limit = %ld;", (long)defaultPragmaJournalSizeLimit]; - status = sqlite3_exec(db, [pragma_journal_size_limit UTF8String], NULL, NULL, NULL); - if (status != SQLITE_OK) { - DDLogError(@"Error setting PRAGMA journal_size_limit: %d %s", status, sqlite3_errmsg(db)); - // This isn't critical, so we can continue. - } - // - // // Set mmap_size (if needed). - // // - // // This configures memory mapped I/O. - // // OWS: we currently don't set options.pragmaMMapSize, so we can ignore this code. - // if (options.pragmaMMapSize > 0) - // { - // NSString *pragma_mmap_size = - // [NSString stringWithFormat:@"PRAGMA mmap_size = %ld;", (long)options.pragmaMMapSize]; - // - // status = sqlite3_exec(db, [pragma_mmap_size UTF8String], NULL, NULL, NULL); - // if (status != SQLITE_OK) - // { - // YDBLogError(@"Error setting PRAGMA mmap_size: %d %s", status, sqlite3_errmsg(db)); - // // This isn't critical, so we can continue. - // } - // } - // - // Disable autocheckpointing. + // ----------------------------------------------------------- // - // YapDatabase has its own optimized checkpointing algorithm built-in. - // It knows the state of every active connection for the database, - // so it can invoke the checkpoint methods at the precise time in which a checkpoint can be most effective. - sqlite3_wal_autocheckpoint(db, 0); - - // END DB setup copied from YapDatabase - // BEGIN SQLCipher migration - - NSString *setPlainTextHeaderPragma = - [NSString stringWithFormat:@"PRAGMA cipher_plaintext_header_size = %d;", kSqliteHeaderLength]; - - status = sqlite3_exec(db, [setPlainTextHeaderPragma UTF8String], NULL, NULL, NULL); - if (status != SQLITE_OK) { - DDLogError(@"Error setting PRAGMA cipher_plaintext_header_size = %d: status: %d, error: %s", - kSqliteHeaderLength, - status, - sqlite3_errmsg(db)); - return OWSErrorWithCodeDescription( - OWSErrorCodeDatabaseConversionFatalError, @"Failed to set PRAGMA cipher_plaintext_header_size"); - } + // SQLCipher migration - // Modify the first page, so that SQLCipher will overwrite, respecting our new cipher_plaintext_header_size - NSString *tableName = [NSString stringWithFormat:@"signal-migration-%@", [NSUUID new].UUIDString]; - NSString *modificationSQL = - [NSString stringWithFormat:@"CREATE TABLE %@(int a); INSERT INTO %@(a) VALUES (1);", tableName, tableName]; - status = sqlite3_exec(db, [modificationSQL UTF8String], NULL, NULL, NULL); - if (status != SQLITE_OK) { - DDLogError(@"%@ Error modifying first page: %d, error: %s", self.logTag, status, sqlite3_errmsg(db)); - return OWSErrorWithCodeDescription(OWSErrorCodeDatabaseConversionFatalError, @"Error modifying first page"); - } + // if (NO) + // { + // + // NSString *setPlainTextHeaderPragma = + // [NSString stringWithFormat:@"PRAGMA cipher_plaintext_header_size = %zd;", kSqliteHeaderLength]; + // + // status = sqlite3_exec(db, [setPlainTextHeaderPragma UTF8String], NULL, NULL, NULL); + // if (status != SQLITE_OK) { + // DDLogError(@"Error setting PRAGMA cipher_plaintext_header_size = %zd: status: %d, error: %s", + // kSqliteHeaderLength, + // status, + // sqlite3_errmsg(db)); + // return OWSErrorWithCodeDescription( + // OWSErrorCodeDatabaseConversionFatalError, @"Failed to set PRAGMA + // cipher_plaintext_header_size"); + // } + // + // // Modify the first page, so that SQLCipher will overwrite, respecting our new cipher_plaintext_header_size + // NSString *tableName = [NSString stringWithFormat:@"signal-migration-%@", [NSUUID new].UUIDString]; + // NSString *modificationSQL = + // [NSString stringWithFormat:@"CREATE TABLE %@(int a); INSERT INTO %@(a) VALUES (1);", tableName, tableName]; + // status = sqlite3_exec(db, [modificationSQL UTF8String], NULL, NULL, NULL); + // if (status != SQLITE_OK) { + // DDLogError(@"%@ Error modifying first page: %d, error: %s", self.logTag, status, sqlite3_errmsg(db)); + // return OWSErrorWithCodeDescription(OWSErrorCodeDatabaseConversionFatalError, @"Error modifying first + // page"); + // } + // + // // Force a checkpoint so that the plaintext is written to the actual DB file, not just living in the WAL. + // // TODO do we need/want the earlier checkpoint if we're checkpointing here? + // sqlite3_wal_autocheckpoint(db, 0); + // + // + // sqlite3_close(db); + // return nil; + // } - // Force a checkpoint so that the plaintext is written to the actual DB file, not just living in the WAL. - // TODO do we need/want the earlier checkpoint if we're checkpointing here? - sqlite3_wal_autocheckpoint(db, 0); + // TODO set plaintext pragma + // TODO modify first page + // TODO force checkpoint return nil; }