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 diff --git a/src/fmdb/FMDatabase.h b/src/fmdb/FMDatabase.h index b779d542..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. @@ -695,6 +701,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 +1005,39 @@ 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 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:(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 890c440b..ce0ed3d5 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,37 @@ - (NSError*)inSavePoint:(void (^)(BOOL *rollback))block { #endif } +- (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, 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); +#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 39e36c18..b4915bf3 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. @@ -217,6 +216,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 +236,38 @@ 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 +///----------------- + +/** 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:(FMDBCheckpointMode)checkpointMode name:(NSString * _Nullable)name logFrameCount:(int * _Nullable)logFrameCount checkpointCount:(int * _Nullable)checkpointCount error:(NSError * _Nullable *)error; + @end NS_ASSUME_NONNULL_END diff --git a/src/fmdb/FMDatabaseQueue.m b/src/fmdb/FMDatabaseQueue.m index f3f30cf2..df993e00 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,31 @@ - (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block { #endif } +- (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 name:name logFrameCount:NULL checkpointCount:NULL error:&blockError]; + }); + FMDBRelease(self); + + if (error) { + *error = blockError; + } + return result; +} + @end