Skip to content

Commit

Permalink
Merge pull request #609 from benasher44/basher/transaction-checkpoint…
Browse files Browse the repository at this point in the history
…-support

Added support for immediate transactions and checkpoint
  • Loading branch information
robertmryan committed Oct 21, 2017
2 parents a76844e + 89d699a commit b4bd097
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 19 deletions.
22 changes: 22 additions & 0 deletions Tests/FMDatabaseTests.m
Expand Up @@ -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
51 changes: 51 additions & 0 deletions src/fmdb/FMDatabase.h
Expand Up @@ -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.
Expand Down Expand Up @@ -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 `<lastError>`, `<lastErrorCode>`, or `<lastErrorMessage>` 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 `<beginTransaction>` or with `<beginDeferredTransaction>`.
Expand Down Expand Up @@ -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
///----------------------------
Expand Down
41 changes: 41 additions & 0 deletions src/fmdb/FMDatabase.m
Expand Up @@ -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"];
Expand Down Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions src/fmdb/FMDatabasePool.h
Expand Up @@ -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.
Expand Down
31 changes: 23 additions & 8 deletions src/fmdb/FMDatabasePool.m
Expand Up @@ -15,6 +15,12 @@
#import "FMDatabasePool.h"
#import "FMDatabase.h"

typedef NS_ENUM(NSInteger, FMDBTransaction) {
FMDBTransactionExclusive,
FMDBTransactionDeferred,
FMDBTransactionImmediate,
};

@interface FMDatabasePool () {
dispatch_queue_t _lockQueue;

Expand Down Expand Up @@ -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;
}


Expand All @@ -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 {
Expand Down
42 changes: 40 additions & 2 deletions src/fmdb/FMDatabaseQueue.h
Expand Up @@ -7,11 +7,10 @@
//

#import <Foundation/Foundation.h>
#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 `<FMDatabase>` from multiple threads at once is a bad idea. It has always been OK to make a `<FMDatabase>` object *per thread*. Just don't share a single instance across threads, and definitely not across multiple threads at the same time.
Expand Down Expand Up @@ -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
///-----------------------------------------------
Expand All @@ -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
60 changes: 51 additions & 9 deletions src/fmdb/FMDatabaseQueue.m
Expand Up @@ -15,6 +15,12 @@
#import <sqlite3.h>
#endif

typedef NS_ENUM(NSInteger, FMDBTransaction) {
FMDBTransactionExclusive,
FMDBTransactionDeferred,
FMDBTransactionImmediate,
};

/*
Note: we call [self retain]; before using dispatch_sync, just incase
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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

0 comments on commit b4bd097

Please sign in to comment.