From 2774f91783c53bc78415c510207041db8bfbbc19 Mon Sep 17 00:00:00 2001 From: Ben Asher Date: Fri, 8 Sep 2017 14:12:19 -0700 Subject: [PATCH 1/3] Added support for immediate transactions and checkpoint --- src/fmdb/FMDatabase.h | 26 ++++++++++++++++++++ src/fmdb/FMDatabase.m | 33 +++++++++++++++++++++++++ src/fmdb/FMDatabasePool.h | 7 ++++++ src/fmdb/FMDatabasePool.m | 31 +++++++++++++++++------ src/fmdb/FMDatabaseQueue.h | 21 ++++++++++++++++ src/fmdb/FMDatabaseQueue.m | 50 +++++++++++++++++++++++++++++++------- 6 files changed, 151 insertions(+), 17 deletions(-) diff --git a/src/fmdb/FMDatabase.h b/src/fmdb/FMDatabase.h index b779d542..805b2789 100644 --- a/src/fmdb/FMDatabase.h +++ b/src/fmdb/FMDatabase.h @@ -695,6 +695,18 @@ typedef int(^FMDBExecuteStatementsCallbackBlock)(NSDictionary *resultsDictionary - (BOOL)beginDeferredTransaction; +/** Begin an immediate transaction + + @return `YES` on success; `NO` on failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. + + @see commit + @see rollback + @see beginTransaction + @see isInTransaction + */ + +- (BOOL)beginImmediateTransaction; + /** Commit a transaction Commit a transaction that was initiated with either `` or with ``. @@ -987,6 +999,20 @@ typedef int(^FMDBExecuteStatementsCallbackBlock)(NSDictionary *resultsDictionary - (NSError * _Nullable)inSavePoint:(__attribute__((noescape)) void (^)(BOOL *rollback))block; + +///----------------- +/// @name Checkpoint +///----------------- + +/** Performs a WAL checkpoint + + @param checkpointMode The checkpoint mode for sqlite3_wal_checkpoint_v2 + @param name The db name for sqlite3_wal_checkpoint_v2 + @param error The NSError corresponding to the error, if any. + @return YES on success, otherwise NO. + */ +- (BOOL)checkpoint:(int)checkpointMode dbName:(NSString *)name error:(NSError * _Nullable *)error; + ///---------------------------- /// @name SQLite library status ///---------------------------- diff --git a/src/fmdb/FMDatabase.m b/src/fmdb/FMDatabase.m index 890c440b..9cbdddf1 100644 --- a/src/fmdb/FMDatabase.m +++ b/src/fmdb/FMDatabase.m @@ -1322,6 +1322,16 @@ - (BOOL)beginDeferredTransaction { return b; } +- (BOOL)beginImmediateTransaction { + + BOOL b = [self executeUpdate:@"begin immediate transaction"]; + if (b) { + _isInTransaction = YES; + } + + return b; +} + - (BOOL)beginTransaction { BOOL b = [self executeUpdate:@"begin exclusive transaction"]; @@ -1423,6 +1433,29 @@ - (NSError*)inSavePoint:(void (^)(BOOL *rollback))block { #endif } +- (BOOL)checkpoint:(int)checkpointMode dbName:(NSString *)name error:(NSError * __autoreleasing *)error +{ + const char* dbName = [name UTF8String]; +#if SQLITE_VERSION_NUMBER >= 3007006 + int err = sqlite3_wal_checkpoint_v2(_db, dbName, checkpointMode, NULL, NULL); +#else + NSLog(@"sqlite3_wal_checkpoint_v2 unavailable before sqlite 3.7.6. Ignoring checkpoint mode: %d", mode); + int err = sqlite3_wal_checkpoint(_db, dbName); +#endif + if(err != SQLITE_OK) { + if (error) { + *error = [self lastError]; + } + if (self.logsErrors) NSLog(@"%@", [self lastErrorMessage]); + if (self.crashOnErrors) { + NSAssert(false, @"%@", [self lastErrorMessage]); + abort(); + } + return NO; + } else { + return YES; + } +} #pragma mark Cache statements diff --git a/src/fmdb/FMDatabasePool.h b/src/fmdb/FMDatabasePool.h index 3642f59c..831d3f88 100755 --- a/src/fmdb/FMDatabasePool.h +++ b/src/fmdb/FMDatabasePool.h @@ -212,6 +212,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)inDeferredTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block; +/** Synchronously perform database operations on queue, using immediate transactions. + + @param block The code to be run on the queue of `FMDatabaseQueue` + */ + +- (void)inImmediateTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block; + /** Synchronously perform database operations in pool using save point. @param block The code to be run on the `FMDatabasePool` pool. diff --git a/src/fmdb/FMDatabasePool.m b/src/fmdb/FMDatabasePool.m index 41a59858..33f0cf2b 100755 --- a/src/fmdb/FMDatabasePool.m +++ b/src/fmdb/FMDatabasePool.m @@ -15,6 +15,12 @@ #import "FMDatabasePool.h" #import "FMDatabase.h" +typedef NS_ENUM(NSInteger, FMDBTransaction) { + FMDBTransactionExclusive, + FMDBTransactionDeferred, + FMDBTransactionImmediate, +}; + @interface FMDatabasePool () { dispatch_queue_t _lockQueue; @@ -244,17 +250,22 @@ - (void)inDatabase:(void (^)(FMDatabase *db))block { [self pushDatabaseBackInPool:db]; } -- (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block { +- (void)beginTransaction:(FMDBTransaction)transaction withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block { BOOL shouldRollback = NO; FMDatabase *db = [self db]; - if (useDeferred) { - [db beginDeferredTransaction]; - } - else { - [db beginTransaction]; + switch (transaction) { + case FMDBTransactionExclusive: + [db beginTransaction]; + break; + case FMDBTransactionDeferred: + [db beginDeferredTransaction]; + break; + case FMDBTransactionImmediate: + [db beginImmediateTransaction]; + break; } @@ -271,11 +282,15 @@ - (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, B } - (void)inDeferredTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block { - [self beginTransaction:YES withBlock:block]; + [self beginTransaction:FMDBTransactionDeferred withBlock:block]; } - (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block { - [self beginTransaction:NO withBlock:block]; + [self beginTransaction:FMDBTransactionExclusive withBlock:block]; +} + +- (void)inImmediateTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block { + [self beginTransaction:FMDBTransactionImmediate withBlock:block]; } - (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block { diff --git a/src/fmdb/FMDatabaseQueue.h b/src/fmdb/FMDatabaseQueue.h index 0a8b9381..aadea321 100755 --- a/src/fmdb/FMDatabaseQueue.h +++ b/src/fmdb/FMDatabaseQueue.h @@ -217,6 +217,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)inDeferredTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block; +/** Synchronously perform database operations on queue, using immediate transactions. + + @param block The code to be run on the queue of `FMDatabaseQueue` + */ + +- (void)inImmediateTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block; + ///----------------------------------------------- /// @name Dispatching database operations to queue ///----------------------------------------------- @@ -230,6 +237,20 @@ NS_ASSUME_NONNULL_BEGIN // If you need to nest, use FMDatabase's startSavePointWithName:error: instead. - (NSError * _Nullable)inSavePoint:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block; +///----------------- +/// @name Checkpoint +///----------------- + +/** Synchronously performs a WAL checkpoint + + @param checkpointMode The checkpoint mode for sqlite3_wal_checkpoint_v2 + @param name The db name for sqlite3_wal_checkpoint_v2 + @param error The NSError corresponding to the error, if any. + @return YES on success, otherwise NO. + */ + +- (BOOL)checkpoint:(int)checkpointMode dbName:(NSString *)name error:(NSError * _Nullable *)error; + @end NS_ASSUME_NONNULL_END diff --git a/src/fmdb/FMDatabaseQueue.m b/src/fmdb/FMDatabaseQueue.m index f3f30cf2..77f90663 100755 --- a/src/fmdb/FMDatabaseQueue.m +++ b/src/fmdb/FMDatabaseQueue.m @@ -15,6 +15,12 @@ #import #endif +typedef NS_ENUM(NSInteger, FMDBTransaction) { + FMDBTransactionExclusive, + FMDBTransactionDeferred, + FMDBTransactionImmediate, +}; + /* Note: we call [self retain]; before using dispatch_sync, just incase @@ -201,17 +207,22 @@ - (void)inDatabase:(void (^)(FMDatabase *db))block { FMDBRelease(self); } -- (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block { +- (void)beginTransaction:(FMDBTransaction)transaction withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block { FMDBRetain(self); dispatch_sync(_queue, ^() { BOOL shouldRollback = NO; - - if (useDeferred) { - [[self database] beginDeferredTransaction]; - } - else { - [[self database] beginTransaction]; + + switch (transaction) { + case FMDBTransactionExclusive: + [[self database] beginTransaction]; + break; + case FMDBTransactionDeferred: + [[self database] beginDeferredTransaction]; + break; + case FMDBTransactionImmediate: + [[self database] beginImmediateTransaction]; + break; } block([self database], &shouldRollback); @@ -228,11 +239,15 @@ - (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, B } - (void)inDeferredTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block { - [self beginTransaction:YES withBlock:block]; + [self beginTransaction:FMDBTransactionDeferred withBlock:block]; } - (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block { - [self beginTransaction:NO withBlock:block]; + [self beginTransaction:FMDBTransactionExclusive withBlock:block]; +} + +- (void)inImmediateTransaction:(void (^)(FMDatabase * _Nonnull, BOOL * _Nonnull))block { + [self beginTransaction:FMDBTransactionImmediate withBlock:block]; } - (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block { @@ -267,4 +282,21 @@ - (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block { #endif } +- (BOOL)checkpoint:(int)mode dbName:(NSString *)name error:(NSError * __autoreleasing *)error +{ + __block BOOL result; + __block NSError *blockError; + + FMDBRetain(self); + dispatch_sync(_queue, ^() { + result = [self.database checkpoint:mode dbName:name error:&blockError]; + }); + FMDBRelease(self); + + if (error) { + *error = blockError; + } + return result; +} + @end From 577e4f2c22ab7a8369d7dbb60f8443c2ca18e8ee Mon Sep 17 00:00:00 2001 From: "Robert M. Ryan" Date: Fri, 8 Sep 2017 19:26:13 -0700 Subject: [PATCH 2/3] Checkpoint tweaks 1. If we're going to expose `sqlite3_wal_checkpoint_v2` wrapper, I'd suggest a `typedef` for the checkpoint modes rather than `int`. Swift 2/3 users don't have easy access to `sqlite3.h`, so we want to show them their options. It leads to more natural, less cryptic Swift code. 2. In the checkpoint method, I might suggest `name` instead of `dbName`. 3. If we're providing access to `sqlite3_wal_checkpoint_v2`, we might as well provide access to those last two parameters, too. Let's provide a version with fewer parameters so not everyone is encumbered with that. 4. In the checkpoint method, the name is `_Nullable` (as are the two additional parameters). --- src/fmdb/FMDatabase.h | 27 ++++++++++++++++++++++++++- src/fmdb/FMDatabase.m | 12 ++++++++++-- src/fmdb/FMDatabaseQueue.h | 25 +++++++++++++++++++++---- src/fmdb/FMDatabaseQueue.m | 18 ++++++++++++++---- 4 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/fmdb/FMDatabase.h b/src/fmdb/FMDatabase.h index 805b2789..f5ad036b 100644 --- a/src/fmdb/FMDatabase.h +++ b/src/fmdb/FMDatabase.h @@ -41,6 +41,12 @@ NS_ASSUME_NONNULL_BEGIN typedef int(^FMDBExecuteStatementsCallbackBlock)(NSDictionary *resultsDictionary); +typedef NS_ENUM(int, FMDBCheckpointMode) { + FMDBCheckpointModePassive = 0, // SQLITE_CHECKPOINT_PASSIVE, + FMDBCheckpointModeFull = 1, // SQLITE_CHECKPOINT_FULL, + FMDBCheckpointModeRestart = 2, // SQLITE_CHECKPOINT_RESTART, + FMDBCheckpointModeTruncate = 3 // SQLITE_CHECKPOINT_TRUNCATE +}; /** A SQLite ([http://sqlite.org/](http://sqlite.org/)) Objective-C wrapper. @@ -1005,13 +1011,32 @@ typedef int(^FMDBExecuteStatementsCallbackBlock)(NSDictionary *resultsDictionary ///----------------- /** Performs a WAL checkpoint + + @param checkpointMode The checkpoint mode for sqlite3_wal_checkpoint_v2 + @param error The NSError corresponding to the error, if any. + @return YES on success, otherwise NO. + */ +- (BOOL)checkpoint:(FMDBCheckpointMode)checkpointMode error:(NSError * _Nullable *)error; + +/** Performs a WAL checkpoint + + @param checkpointMode The checkpoint mode for sqlite3_wal_checkpoint_v2 + @param name The db name for sqlite3_wal_checkpoint_v2 + @param error The NSError corresponding to the error, if any. + @return YES on success, otherwise NO. + */ +- (BOOL)checkpoint:(FMDBCheckpointMode)checkpointMode name:(NSString * _Nullable)name error:(NSError * _Nullable *)error; +/** Performs a WAL checkpoint + @param checkpointMode The checkpoint mode for sqlite3_wal_checkpoint_v2 @param name The db name for sqlite3_wal_checkpoint_v2 @param error The NSError corresponding to the error, if any. + @param logFrameCount If not NULL, then this is set to the total number of frames in the log file or to -1 if the checkpoint could not run because of an error or because the database is not in WAL mode. + @param checkpointCount If not NULL, then this is set to the total number of checkpointed frames in the log file (including any that were already checkpointed before the function was called) or to -1 if the checkpoint could not run due to an error or because the database is not in WAL mode. @return YES on success, otherwise NO. */ -- (BOOL)checkpoint:(int)checkpointMode dbName:(NSString *)name error:(NSError * _Nullable *)error; +- (BOOL)checkpoint:(FMDBCheckpointMode)checkpointMode name:(NSString * _Nullable)name logFrameCount:(int * _Nullable)logFrameCount checkpointCount:(int * _Nullable)checkpointCount error:(NSError * _Nullable *)error; ///---------------------------- /// @name SQLite library status diff --git a/src/fmdb/FMDatabase.m b/src/fmdb/FMDatabase.m index 9cbdddf1..ce0ed3d5 100644 --- a/src/fmdb/FMDatabase.m +++ b/src/fmdb/FMDatabase.m @@ -1433,11 +1433,19 @@ - (NSError*)inSavePoint:(void (^)(BOOL *rollback))block { #endif } -- (BOOL)checkpoint:(int)checkpointMode dbName:(NSString *)name error:(NSError * __autoreleasing *)error +- (BOOL)checkpoint:(FMDBCheckpointMode)checkpointMode error:(NSError * __autoreleasing *)error { + return [self checkpoint:checkpointMode name:nil logFrameCount:NULL checkpointCount:NULL error:error]; +} + +- (BOOL)checkpoint:(FMDBCheckpointMode)checkpointMode name:(NSString *)name error:(NSError * __autoreleasing *)error { + return [self checkpoint:checkpointMode name:name logFrameCount:NULL checkpointCount:NULL error:error]; +} + +- (BOOL)checkpoint:(FMDBCheckpointMode)checkpointMode name:(NSString *)name logFrameCount:(int *)logFrameCount checkpointCount:(int *)checkpointCount error:(NSError * __autoreleasing *)error { const char* dbName = [name UTF8String]; #if SQLITE_VERSION_NUMBER >= 3007006 - int err = sqlite3_wal_checkpoint_v2(_db, dbName, checkpointMode, NULL, NULL); + int err = sqlite3_wal_checkpoint_v2(_db, dbName, checkpointMode, logFrameCount, checkpointCount); #else NSLog(@"sqlite3_wal_checkpoint_v2 unavailable before sqlite 3.7.6. Ignoring checkpoint mode: %d", mode); int err = sqlite3_wal_checkpoint(_db, dbName); diff --git a/src/fmdb/FMDatabaseQueue.h b/src/fmdb/FMDatabaseQueue.h index aadea321..24ec6eaa 100755 --- a/src/fmdb/FMDatabaseQueue.h +++ b/src/fmdb/FMDatabaseQueue.h @@ -7,11 +7,10 @@ // #import +#import "FMDatabase.h" NS_ASSUME_NONNULL_BEGIN -@class FMDatabase; - /** To perform queries and updates on multiple threads, you'll want to use `FMDatabaseQueue`. Using a single instance of `` from multiple threads at once is a bad idea. It has always been OK to make a `` object *per thread*. Just don't share a single instance across threads, and definitely not across multiple threads at the same time. @@ -241,15 +240,33 @@ NS_ASSUME_NONNULL_BEGIN /// @name Checkpoint ///----------------- -/** Synchronously performs a WAL checkpoint +/** Performs a WAL checkpoint + + @param checkpointMode The checkpoint mode for sqlite3_wal_checkpoint_v2 + @param error The NSError corresponding to the error, if any. + @return YES on success, otherwise NO. + */ +- (BOOL)checkpoint:(FMDBCheckpointMode)checkpointMode error:(NSError * _Nullable *)error; +/** Performs a WAL checkpoint + @param checkpointMode The checkpoint mode for sqlite3_wal_checkpoint_v2 @param name The db name for sqlite3_wal_checkpoint_v2 @param error The NSError corresponding to the error, if any. @return YES on success, otherwise NO. */ +- (BOOL)checkpoint:(FMDBCheckpointMode)checkpointMode name:(NSString * _Nullable)name error:(NSError * _Nullable *)error; -- (BOOL)checkpoint:(int)checkpointMode dbName:(NSString *)name error:(NSError * _Nullable *)error; +/** Performs a WAL checkpoint + + @param checkpointMode The checkpoint mode for sqlite3_wal_checkpoint_v2 + @param name The db name for sqlite3_wal_checkpoint_v2 + @param error The NSError corresponding to the error, if any. + @param logFrameCount If not NULL, then this is set to the total number of frames in the log file or to -1 if the checkpoint could not run because of an error or because the database is not in WAL mode. + @param checkpointCount If not NULL, then this is set to the total number of checkpointed frames in the log file (including any that were already checkpointed before the function was called) or to -1 if the checkpoint could not run due to an error or because the database is not in WAL mode. + @return YES on success, otherwise NO. + */ +- (BOOL)checkpoint:(FMDBCheckpointMode)checkpointMode name:(NSString * _Nullable)name logFrameCount:(int * _Nullable)logFrameCount checkpointCount:(int * _Nullable)checkpointCount error:(NSError * _Nullable *)error; @end diff --git a/src/fmdb/FMDatabaseQueue.m b/src/fmdb/FMDatabaseQueue.m index 77f90663..df993e00 100755 --- a/src/fmdb/FMDatabaseQueue.m +++ b/src/fmdb/FMDatabaseQueue.m @@ -282,17 +282,27 @@ - (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block { #endif } -- (BOOL)checkpoint:(int)mode dbName:(NSString *)name error:(NSError * __autoreleasing *)error +- (BOOL)checkpoint:(FMDBCheckpointMode)mode error:(NSError * __autoreleasing *)error +{ + return [self checkpoint:mode name:nil logFrameCount:NULL checkpointCount:NULL error:error]; +} + +- (BOOL)checkpoint:(FMDBCheckpointMode)mode name:(NSString *)name error:(NSError * __autoreleasing *)error +{ + return [self checkpoint:mode name:name logFrameCount:NULL checkpointCount:NULL error:error]; +} + +- (BOOL)checkpoint:(FMDBCheckpointMode)mode name:(NSString *)name logFrameCount:(int * _Nullable)logFrameCount checkpointCount:(int * _Nullable)checkpointCount error:(NSError * __autoreleasing _Nullable * _Nullable)error { __block BOOL result; __block NSError *blockError; - + FMDBRetain(self); dispatch_sync(_queue, ^() { - result = [self.database checkpoint:mode dbName:name error:&blockError]; + result = [self.database checkpoint:mode name:name logFrameCount:NULL checkpointCount:NULL error:&blockError]; }); FMDBRelease(self); - + if (error) { *error = blockError; } From 89d699a694f6fbbf3da21cefc9decc81aaa37e04 Mon Sep 17 00:00:00 2001 From: Ben Asher Date: Mon, 11 Sep 2017 13:54:10 -0700 Subject: [PATCH 3/3] Add tests --- Tests/FMDatabaseTests.m | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Tests/FMDatabaseTests.m b/Tests/FMDatabaseTests.m index e99f888e..a75b9ac0 100644 --- a/Tests/FMDatabaseTests.m +++ b/Tests/FMDatabaseTests.m @@ -1464,4 +1464,26 @@ - (void)testStepError { XCTAssertEqual(error.code, 19, @"error code 19 should have been generated"); } +- (void)testCheckpoint { + FMDatabase *db = [[FMDatabase alloc] init]; + XCTAssertTrue([db open], @"open failed"); + NSError *error = nil; + int frameCount = 0; + int checkpointCount = 0; + [db checkpoint:FMDBCheckpointModeTruncate name:NULL logFrameCount:&frameCount checkpointCount:&checkpointCount error:&error]; + // Verify that we're calling the checkpoint interface, which is a decent scope for this test, without going so far as to verify what checkpoint does + XCTAssertEqual(frameCount, -1, @"frameCount should be -1 (means not using WAL mode) to verify that we're using the proper checkpoint interface"); + XCTAssertEqual(checkpointCount, -1, @"checkpointCount should be -1 (means not using WAL mode) to verify that we're using the proper checkpoint interface"); +} + +- (void)testImmediateTransaction { + FMDatabase *db = [[FMDatabase alloc] init]; + XCTAssertTrue([db open], @"open failed"); + [db beginImmediateTransaction]; + [db beginImmediateTransaction]; + + // Verify that beginImmediateTransaction behaves as advertised and starts a transaction + XCTAssertEqualObjects([db lastError].localizedDescription, @"cannot start a transaction within a transaction"); +} + @end