From 342bab7dd5a921f56e6bea4e9b0ab84273f1b0d1 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 18 Dec 2014 18:26:49 -0800 Subject: [PATCH] Add object transaction and read-only database pool --- fmdb.xcodeproj/project.pbxproj | 20 +++ src/fmdb/FMDB.h | 2 + src/fmdb/FMDatabaseTransaction.h | 103 ++++++++++++ src/fmdb/FMDatabaseTransaction.m | 198 +++++++++++++++++++++++ src/fmdb/FMReadOnlyDatabasePool.h | 78 ++++++++++ src/fmdb/FMReadOnlyDatabasePool.m | 251 ++++++++++++++++++++++++++++++ 6 files changed, 652 insertions(+) create mode 100644 src/fmdb/FMDatabaseTransaction.h create mode 100644 src/fmdb/FMDatabaseTransaction.m create mode 100644 src/fmdb/FMReadOnlyDatabasePool.h create mode 100644 src/fmdb/FMReadOnlyDatabasePool.m diff --git a/fmdb.xcodeproj/project.pbxproj b/fmdb.xcodeproj/project.pbxproj index 6dc450f7..796dc1bd 100644 --- a/fmdb.xcodeproj/project.pbxproj +++ b/fmdb.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 25BBB47A1A43C422003CA4CE /* FMReadOnlyDatabasePool.h in Headers */ = {isa = PBXBuildFile; fileRef = 25BBB4761A43C422003CA4CE /* FMReadOnlyDatabasePool.h */; }; + 25BBB47B1A43C422003CA4CE /* FMReadOnlyDatabasePool.m in Sources */ = {isa = PBXBuildFile; fileRef = 25BBB4771A43C422003CA4CE /* FMReadOnlyDatabasePool.m */; }; + 25BBB47C1A43C422003CA4CE /* FMReadOnlyDatabasePool.m in Sources */ = {isa = PBXBuildFile; fileRef = 25BBB4771A43C422003CA4CE /* FMReadOnlyDatabasePool.m */; }; + 25BBB47D1A43C422003CA4CE /* FMDatabaseTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = 25BBB4781A43C422003CA4CE /* FMDatabaseTransaction.h */; }; + 25BBB47E1A43C422003CA4CE /* FMDatabaseTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 25BBB4791A43C422003CA4CE /* FMDatabaseTransaction.m */; }; + 25BBB47F1A43C422003CA4CE /* FMDatabaseTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 25BBB4791A43C422003CA4CE /* FMDatabaseTransaction.m */; }; 3354379C19E71096005661F3 /* FMResultSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3354379B19E71096005661F3 /* FMResultSetTests.m */; }; 621721B21892BFE30006691F /* FMDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = CCC24EBB0A13E34D00A6D3E3 /* FMDatabase.m */; }; 621721B31892BFE30006691F /* FMResultSet.m in Sources */ = {isa = PBXBuildFile; fileRef = CCC24EC00A13E34D00A6D3E3 /* FMResultSet.m */; }; @@ -88,6 +94,10 @@ /* Begin PBXFileReference section */ 08FB779EFE84155DC02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 25BBB4761A43C422003CA4CE /* FMReadOnlyDatabasePool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FMReadOnlyDatabasePool.h; path = src/fmdb/FMReadOnlyDatabasePool.h; sourceTree = ""; }; + 25BBB4771A43C422003CA4CE /* FMReadOnlyDatabasePool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FMReadOnlyDatabasePool.m; path = src/fmdb/FMReadOnlyDatabasePool.m; sourceTree = ""; }; + 25BBB4781A43C422003CA4CE /* FMDatabaseTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FMDatabaseTransaction.h; path = src/fmdb/FMDatabaseTransaction.h; sourceTree = ""; }; + 25BBB4791A43C422003CA4CE /* FMDatabaseTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FMDatabaseTransaction.m; path = src/fmdb/FMDatabaseTransaction.m; sourceTree = ""; }; 32A70AAB03705E1F00C91783 /* fmdb_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = fmdb_Prefix.pch; path = src/sample/fmdb_Prefix.pch; sourceTree = SOURCE_ROOT; }; 3354379B19E71096005661F3 /* FMResultSetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMResultSetTests.m; sourceTree = ""; }; 6290CBB5188FE836009790F8 /* libFMDB-IOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libFMDB-IOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -241,6 +251,10 @@ 8314AF3018CD737D00EC0E25 /* fmdb */ = { isa = PBXGroup; children = ( + 25BBB4761A43C422003CA4CE /* FMReadOnlyDatabasePool.h */, + 25BBB4771A43C422003CA4CE /* FMReadOnlyDatabasePool.m */, + 25BBB4781A43C422003CA4CE /* FMDatabaseTransaction.h */, + 25BBB4791A43C422003CA4CE /* FMDatabaseTransaction.m */, 8314AF3218CD73D600EC0E25 /* FMDB.h */, CCC24EBA0A13E34D00A6D3E3 /* FMDatabase.h */, CCC24EBB0A13E34D00A6D3E3 /* FMDatabase.m */, @@ -343,6 +357,8 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 25BBB47D1A43C422003CA4CE /* FMDatabaseTransaction.h in Headers */, + 25BBB47A1A43C422003CA4CE /* FMReadOnlyDatabasePool.h in Headers */, EE42910712B42FC90088BD94 /* FMDatabase.h in Headers */, EE42910612B42FC30088BD94 /* FMDatabaseAdditions.h in Headers */, EE42910912B42FD00088BD94 /* FMResultSet.h in Headers */, @@ -478,6 +494,8 @@ buildActionMask = 2147483647; files = ( 621721B31892BFE30006691F /* FMResultSet.m in Sources */, + 25BBB47F1A43C422003CA4CE /* FMDatabaseTransaction.m in Sources */, + 25BBB47C1A43C422003CA4CE /* FMReadOnlyDatabasePool.m in Sources */, 621721B21892BFE30006691F /* FMDatabase.m in Sources */, 621721B61892BFE30006691F /* FMDatabasePool.m in Sources */, 621721B41892BFE30006691F /* FMDatabaseQueue.m in Sources */, @@ -523,6 +541,8 @@ buildActionMask = 2147483647; files = ( EE42910812B42FCC0088BD94 /* FMDatabase.m in Sources */, + 25BBB47E1A43C422003CA4CE /* FMDatabaseTransaction.m in Sources */, + 25BBB47B1A43C422003CA4CE /* FMReadOnlyDatabasePool.m in Sources */, EE42910512B42FBC0088BD94 /* FMDatabaseAdditions.m in Sources */, EE42910A12B42FD20088BD94 /* FMResultSet.m in Sources */, CC9E4EBB13B31188005F9210 /* FMDatabasePool.m in Sources */, diff --git a/src/fmdb/FMDB.h b/src/fmdb/FMDB.h index 39e2f431..ba5fe73d 100644 --- a/src/fmdb/FMDB.h +++ b/src/fmdb/FMDB.h @@ -3,3 +3,5 @@ #import "FMDatabaseAdditions.h" #import "FMDatabaseQueue.h" #import "FMDatabasePool.h" +#import "FMDatabaseTransaction.h" +#import "FMReadOnlyDatabasePool.h" diff --git a/src/fmdb/FMDatabaseTransaction.h b/src/fmdb/FMDatabaseTransaction.h new file mode 100644 index 00000000..5eece482 --- /dev/null +++ b/src/fmdb/FMDatabaseTransaction.h @@ -0,0 +1,103 @@ +// +// FMDatabaseTransaction.h +// LayerKit +// +// Created by Blake Watters on 12/18/14. +// Copyright (c) 2014 Layer Inc. All rights reserved. +// + +#import + +/** + @abstract The `FMDatabaseTransaction` class models a SQLite database transaction as an object. Write access is brokered via a semaphore. + @discussion The `FMDatabaseTransaction` object is an interface for mediating threaded access to a single writable database instance. The semaphore + guarantees that only a single consumer can access the database at a time without requiring the use of blocks to mediate access. Note that transaction + objects are single use. Attempting to reuse a transaction object after it has been committed or rolled back is programmer error and will result in a + runtime exception being raised. + */ +@interface FMDatabaseTransaction : NSObject + +///-------------------------------------- +/// @name Creating a Database Transaction +///-------------------------------------- + +/** + @abstract Creates and returns a new transaction for the given database. A shared semaphore with a count of 1 must be used to broker access + between all transaction objects. + @param database The writable database to create a transaction against. + @param semaphore A Grand Central Dispatch counting semaphore used to broker access to the database. + @return A newly created database transaction object. + */ ++ (instancetype)transactionWithDatabase:(FMDatabase *)database semaphore:(dispatch_semaphore_t)semaphore; + +/** + @abstract Returns the database the transaction is bound to or `nil` if it has not yet been opened. + @discussion It is not safe to utilize the database reference until the transaction has been opened. The accessor will return `nil` until the transaction is opened. + */ +@property (nonatomic, readonly) FMDatabase *database; + +///----------------------------------- +/// @name Inspecting Transaction State +///----------------------------------- + +/** + @abstract Returns a Boolean value that indicates if the transaction has been opened. + @discussion The transaction is only considered opened if the underlying database is in a transaction and the receiver opened the transaction. + */ +@property (nonatomic, readonly) BOOL isOpen; + +/** + @abstract Returns a Boolean value that indicates if the transaction is complete (from being committed or rolled back). + */ +@property (nonatomic, readonly) BOOL isComplete; + +///------------------------------------------ +/// @name Opening and Closing the Transaction +///------------------------------------------ + +/** + @abstract Opens the database transaction, gaining exclusive write access to the connection. + @param deferred A Boolean value that determine if an exclusive or a deferred transaction is opened. + @param error A pointer to an error object that is set upon failure to open the transaction. + @return A Boolean value that indicates if the transaction was successfully opened. + */ +- (BOOL)open:(BOOL)deferred error:(NSError **)error; + +/** + @abstract Commits an open transaction. + @param error A pointer to an error object that is set upon failure to commit the transaction. + @return A Boolean value that indicates if the transaction was committed successfully. + */ +- (BOOL)commit:(NSError **)error; + +/** + @abstract Rolls back an open transaction. + @param error A pointer to an error object that is set upon failure to roll back the transaction. + @return A Boolean value that indicates if the transaction was rolled back successfully. + */ +- (BOOL)rollback:(NSError **)error; + +///------------------------------------------ +/// @name Executing a Transaction via a Block +///------------------------------------------ + +/** + @abstract Executes the block in between a "transaction begin" and "transaction commit" statements. + @param database Database reference. + @param transactionBlock Transaction block with a `LYRDatabase` instance and a pointer to the `shouldRollback` switch. + */ +- (BOOL)performTransactionWithBlock:(void (^)(FMDatabase *database, BOOL *shouldRollback))transactionBlock; + +///------------------------------------- +/// @name Configuring a Completion Block +///------------------------------------- + +/** + @abstract Sets a completion block that gets executed after the transaction has been commited, rolled back or aborted. + @discussion If set, the completion block is guaranteed to be executed. If the transaction is aborted by falling out of scope then the completion block is invoked during `dealloc`. + If the transaction is aborted, then the completion block is called with a `isCommitted` value of `NO` and a `nil` error. + @param completion A block to executed upon completion of the transaction. + */ +- (void)setCompletionBlock:(void (^)(BOOL isCommitted, NSError *error))completionBlock; + +@end diff --git a/src/fmdb/FMDatabaseTransaction.m b/src/fmdb/FMDatabaseTransaction.m new file mode 100644 index 00000000..aacf3b81 --- /dev/null +++ b/src/fmdb/FMDatabaseTransaction.m @@ -0,0 +1,198 @@ +// +// FMDatabaseTransaction.m +// LayerKit +// +// Created by Blake Watters on 12/18/14. +// Copyright (c) 2014 Layer Inc. All rights reserved. +// + +#import "FMDatabaseTransaction.h" + +@interface FMDatabaseTransaction () +@property (nonatomic) FMDatabase *database; +@property (nonatomic) dispatch_semaphore_t semaphore; +@property (nonatomic) NSRecursiveLock *lock; +@property (nonatomic, copy) void (^completionBlock)(BOOL isCommitted, NSError *error); +@property (nonatomic) BOOL openedTransaction; +@end + +@implementation FMDatabaseTransaction + ++ (instancetype)transactionWithDatabase:(FMDatabase *)database semaphore:(dispatch_semaphore_t)semaphore +{ + return [[self alloc] initWithDatabase:database semaphore:semaphore]; +} + +- (id)initWithDatabase:(FMDatabase *)database semaphore:(dispatch_semaphore_t)semaphore +{ + NSParameterAssert(database); + NSParameterAssert(semaphore); + self = [super init]; + if (self) { + _database = database; + _semaphore = semaphore; + _lock = [NSRecursiveLock new]; + _lock.name = [NSString stringWithFormat:@"LYRDatabaseTransaction Lock %p", self]; + _openedTransaction = NO; + } + return self; +} + +- (id)init +{ + @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Failed to call designated initializer." userInfo:nil]; +} + +- (void)dealloc +{ + // Only signal and dispatch completion if we opened the transaction + if (_openedTransaction) { + if (_semaphore) { + if (self.isOpen) { + [self rollback:nil]; + } else { + dispatch_semaphore_signal(_semaphore); + [self signalCompletionWithSuccess:NO error:nil]; + } + } else { + [self signalCompletionWithSuccess:NO error:nil]; + } + } +} + +- (BOOL)performTransactionWithBlock:(void (^)(FMDatabase *database, BOOL *shouldRollback))transactionBlock +{ + if (!transactionBlock) { + @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Failed to perform a transaction with a nil block." userInfo:nil]; + } + + // Begin transaction + BOOL success = [self open:YES error:nil]; + if (!success) { + return NO; + } + + // Execute transaction block + BOOL shouldRollback = NO; + transactionBlock(_database, &shouldRollback); + + // Check if rollback is needed + if (shouldRollback) { + success = [self rollback:nil]; + } else { + success = [self commit:nil]; + } + return success; +} + +- (BOOL)open:(BOOL)deferred error:(NSError **)error +{ + if (!_semaphore) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"No semaphore available: database transaction objects cannot be reused." userInfo:nil]; + if (!_database) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"No database available: database transaction objects cannot be reused." userInfo:nil]; + + dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); + [self.lock lock]; + if (self.database.inTransaction) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot open transaction because it has already been opened." userInfo:nil]; + BOOL success = deferred ? [_database beginDeferredTransaction] : [_database beginTransaction]; + if (success) { + self.openedTransaction = YES; + } else { + if (error) { + *error = [self.database lastError]; + } + dispatch_semaphore_signal(_semaphore); + } + [self.lock unlock]; + return success; +} + +- (FMDatabase *)database +{ + if (self.isOpen) { + return _database; + } else { + return nil; + } +} + +- (BOOL)isOpen +{ + [self.lock lock]; + BOOL isOpen = self.openedTransaction && _database.inTransaction; + [self.lock unlock]; + return isOpen; +} + +- (void)releaseDatabase +{ + [_database closeOpenResultSets]; + _database = nil; + _isComplete = YES; + dispatch_semaphore_signal(_semaphore); + _semaphore = nil; +} + +- (BOOL)commit:(NSError **)error +{ + if (!_semaphore) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"No semaphore available: database transaction has already been committed/rolled back." userInfo:nil]; + if (!_database) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"No database available: database transaction has already been committed/rolled back." userInfo:nil]; + + [self.lock lock]; + if (!self.isOpen) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot commit transaction because it is not open." userInfo:nil]; + NSError *outError = nil; + BOOL success = [_database commit]; + if (!success) { + outError = [_database lastError]; + } + [self releaseDatabase]; + [self.lock unlock]; + if (success) { + [self signalCompletionWithSuccess:YES error:nil]; + } else { + [self signalCompletionWithSuccess:YES error:outError]; + } + if (error) { + *error = outError; + } + return success; +} + +- (BOOL)rollback:(NSError **)error +{ + if (!_semaphore) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"No semaphore available: database transaction has already been committed/rolled back." userInfo:nil]; + if (!_database) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"No database available: database transaction has already been committed/rolled back." userInfo:nil]; + + [self.lock lock]; + if (!self.isOpen) @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot rollback transaction because it is not open." userInfo:nil]; + NSError *outError = nil; + BOOL success = [_database rollback]; + if (!success) { + outError = [_database lastError]; + } + [self releaseDatabase]; + [self.lock unlock]; + if (success) { + [self signalCompletionWithSuccess:NO error:nil]; + } else { + [self signalCompletionWithSuccess:NO error:outError]; + } + if (error) { + *error = outError; + } + return success; +} + +- (void)setCompletionBlock:(void (^)(BOOL isCommitted, NSError *error))completionBlock +{ + _completionBlock = [completionBlock copy]; +} + +- (void)signalCompletionWithSuccess:(BOOL)success error:(NSError *)error +{ + if (self.completionBlock) { + self.completionBlock(success, error); + self.completionBlock = nil; + } +} + +@end diff --git a/src/fmdb/FMReadOnlyDatabasePool.h b/src/fmdb/FMReadOnlyDatabasePool.h new file mode 100644 index 00000000..4b36a802 --- /dev/null +++ b/src/fmdb/FMReadOnlyDatabasePool.h @@ -0,0 +1,78 @@ +// +// FMReadOnlyDatabasePool.h +// LayerKit +// +// Created by Blake Watters on 12/18/14. +// Copyright (c) 2014 Layer Inc. All rights reserved. +// + +#import + +/* + @abstract The `FMReadOnlyDatabasePool` class provides a factory of database + transactions while maintaining a finite pool of database connections. + */ +@interface FMReadOnlyDatabasePool : NSObject + +/** + @abstract Creates and returns a database pool with a given number of database connections. + @discussion This method takes care of initializing and opening multiple database connections with given flags and puts them in a managed pool. + @param path The path to the database on disk. Passing `nil` causes an exception. + @param flags Flags that get passed down to the sqlite3_open function call. + @param numberOfDatabases Pool size with the number of open database connection. + @return A newly construct database pool manager object initialized with a given number of database connections. + */ ++ (instancetype)databasePoolWithPath:(NSString *)path flags:(int)flags capacity:(NSUInteger)numberOfDatabases; + +///------------------------------------------------ +/// @name Inspecting Pool Capacity and Availability +///------------------------------------------------ + +/** + @abstract Returns the number of databases in the pool. + */ +@property (nonatomic, readonly) NSUInteger numberOfDatabases; + +/** + @abstract Returns the number of databases available for utilization in the pool (from the maximum of `numberOfDatabases`). + */ +@property (nonatomic, readonly) NSUInteger numberOfAvailableDatabases; + +///------------------------------------ +/// @name Acquiring Available Databases +///------------------------------------ + +/** + @abstract Returns an available database from the pool. The database connection will not be vended to any other listener as long as the returned reference + is kept alive by the caller. + @return An available read-only database connection or `nil` if none is available. + */ +- (FMDatabase *)availableDatabase; + +/** + @abstract Blocks the caller until a databases connection is available. + @return An available read-only database connection. + */ +- (FMDatabase *)waitForAvailableDatabase; + +///------------------------------------- +/// @name Explicitly Releasing Databases +///------------------------------------- + +/** + @abstract Explicitly releases the given database back to the pool. + @discussion After invoking this method the caller must guarantee that it will no longer execute any queries against the database object as it will be immediately returned to the available connection pool and may be vended to another consumer. + */ +- (void)releaseDatabase:(FMDatabase *)database; + +///------------------------------- +/// @name Block Convenience Method +///------------------------------- + +/** + @abstract Acquires an available database and yields it to the block for usage, returning it to the pool after the block has completed. + @param block A block object to execute once a database connection has been acquired. The block has no return value and accepts a single argument: + */ +- (void)inDatabase:(void (^)(FMDatabase *database))block; + +@end diff --git a/src/fmdb/FMReadOnlyDatabasePool.m b/src/fmdb/FMReadOnlyDatabasePool.m new file mode 100644 index 00000000..335e0f39 --- /dev/null +++ b/src/fmdb/FMReadOnlyDatabasePool.m @@ -0,0 +1,251 @@ +// +// FMReadOnlyDatabasePool.m +// LayerKit +// +// Created by Blake Watters on 12/18/14. +// Copyright (c) 2014 Layer Inc. All rights reserved. +// + +#import "FMReadOnlyDatabasePool.h" + +@interface LYRDatabaseProxy : NSObject { + FMDatabase *_database; + dispatch_semaphore_t _semaphore; +} + +- (id)initWithDatabase:(FMDatabase *)database semaphore:(dispatch_semaphore_t)semaphore; +- (void)__waitForSemaphore; + +@end + +@implementation LYRDatabaseProxy + +- (id)initWithDatabase:(FMDatabase *)database semaphore:(dispatch_semaphore_t)semaphore +{ + // NSProxy objects don't respond to `init` + self = [super init]; + if (self) { + _database = database; + _semaphore = semaphore; + } + return self; +} + +- (void)__waitForSemaphore +{ + dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); +} + +- (void)__signalSemaphore +{ + if (_semaphore) { + dispatch_semaphore_signal(_semaphore); + _semaphore = NULL; + } +} + +- (FMDatabase *)__proxiedDatabase +{ + return _database; +} + +- (void)dealloc +{ + if (_database && _semaphore) { + // If we still have a reference to the semaphore then we are deallocating without being cleaned up. Close the open result sets before signaling and returning to the pool + [_database closeOpenResultSets]; + } + [self __signalSemaphore]; +} + +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + if ([_database respondsToSelector:aSelector]) { + return _database; + } + + return self; +} + +- (BOOL)respondsToSelector:(SEL)aSelector +{ + if ([super respondsToSelector:aSelector]) { + return YES; + } else { + if ([_database respondsToSelector:aSelector]) { + return YES; + } + } + return NO; +} + +- (BOOL)isKindOfClass:(Class)aClass +{ + if ([super isKindOfClass:aClass]) { + return YES; + } else { + if ([_database isKindOfClass:aClass]) { + return YES; + } + } + return NO; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p database=%@:%p>", [self class], self, [_database class], _database]; +} + +@end + +@interface FMReadOnlyDatabasePool () + +@property (nonatomic) NSSet *pool; +@property (nonatomic) dispatch_queue_t collectionGuardSerialQueue; +@property (nonatomic) dispatch_semaphore_t poolSemaphore; +@property (nonatomic) NSHashTable *busyDatabases; + +@end + +@implementation FMReadOnlyDatabasePool + ++ (instancetype)databasePoolWithPath:(NSString *)path flags:(int)flags capacity:(NSUInteger)numberOfDatabases +{ + return [[FMReadOnlyDatabasePool alloc] initWithPath:path flags:flags numberOfDatabases:numberOfDatabases]; +} + +- (id)initWithPath:(NSString *)path flags:(int)flags numberOfDatabases:(NSUInteger)numberOfDatabases +{ + // TODO: Verify that flags contains SQLITE_OPEN_READONLY + if (!(flags & SQLITE_OPEN_READONLY)) { + @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Expected flags to include `SQLITE_OPEN_READONLY`" userInfo:nil]; + } + self = [super init]; + if (self) { + if (path == nil) [NSException raise:NSInternalInconsistencyException format:@"Cannot initialize a database pool manager without the `path` parameter."]; + if (numberOfDatabases == 0) [NSException raise:NSInternalInconsistencyException format:@"Cannot initialize a database pool manager with `numberOfDatabases` set to zero."]; + + NSMutableSet *pool = [NSMutableSet setWithCapacity:numberOfDatabases]; + + for (int i=0; i", [self class], self, (unsigned long)self.pool.count, (unsigned long)[self.busyDatabases allObjects].count]; +} + +@end