3415 lines
110 KiB
Objective-C
3415 lines
110 KiB
Objective-C
#import "YapDatabase.h"
|
|
#import "YapDatabaseAtomic.h"
|
|
#import "YapDatabasePrivate.h"
|
|
#import "YapDatabaseExtensionPrivate.h"
|
|
#import "YapCollectionKey.h"
|
|
#import "YapDatabaseManager.h"
|
|
#import "YapDatabaseConnectionState.h"
|
|
#import "YapDatabaseLogging.h"
|
|
#import "YapDatabaseString.h"
|
|
|
|
#import "sqlite3.h"
|
|
|
|
#import <mach/mach_time.h>
|
|
#import <stdatomic.h>
|
|
|
|
#if ! __has_feature(objc_arc)
|
|
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
|
#endif
|
|
|
|
/**
|
|
* Define log level for this file: OFF, ERROR, WARN, INFO, VERBOSE
|
|
* See YapDatabaseLogging.h for more information.
|
|
**/
|
|
#if robbie_hanson
|
|
static const int ydbLogLevel = YDB_LOG_LEVEL_INFO;
|
|
#elif DEBUG
|
|
static const int ydbLogLevel = YDB_LOG_LEVEL_INFO;
|
|
#else
|
|
static const int ydbLogLevel = YDB_LOG_LEVEL_WARN;
|
|
#endif
|
|
#pragma unused(ydbLogLevel)
|
|
|
|
/**
|
|
* YapDatabaseClosedNotification & corresponding keys.
|
|
**/
|
|
|
|
NSString *const YapDatabaseClosedNotification = @"YapDatabaseClosedNotification";
|
|
|
|
NSString *const YapDatabasePathKey = @"databasePath";
|
|
NSString *const YapDatabasePathWalKey = @"databasePath_wal";
|
|
NSString *const YapDatabasePathShmKey = @"databasePath_shm";
|
|
|
|
/**
|
|
* YapDatabaseModifiedNotification & corresponding keys.
|
|
**/
|
|
|
|
NSString *const YapDatabaseModifiedNotification = @"YapDatabaseModifiedNotification";
|
|
NSString *const YapDatabaseModifiedExternallyNotification = @"YapDatabaseModifiedExternallyNotification";
|
|
|
|
NSString *const YapDatabaseSnapshotKey = @"snapshot";
|
|
NSString *const YapDatabaseConnectionKey = @"connection";
|
|
NSString *const YapDatabaseExtensionsKey = @"extensions";
|
|
NSString *const YapDatabaseCustomKey = @"custom";
|
|
|
|
NSString *const YapDatabaseObjectChangesKey = @"objectChanges";
|
|
NSString *const YapDatabaseMetadataChangesKey = @"metadataChanges";
|
|
NSString *const YapDatabaseInsertedKeysKey = @"insertedKeys";
|
|
NSString *const YapDatabaseRemovedKeysKey = @"removedKeys";
|
|
NSString *const YapDatabaseRemovedCollectionsKey = @"removedCollections";
|
|
NSString *const YapDatabaseRemovedRowidsKey = @"removedRowids";
|
|
NSString *const YapDatabaseAllKeysRemovedKey = @"allKeysRemoved";
|
|
NSString *const YapDatabaseModifiedExternallyKey = @"modifiedExternally";
|
|
|
|
NSString *const YapDatabaseRegisteredExtensionsKey = @"registeredExtensions";
|
|
NSString *const YapDatabaseRegisteredMemoryTablesKey = @"registeredMemoryTables";
|
|
NSString *const YapDatabaseExtensionsOrderKey = @"extensionsOrder";
|
|
NSString *const YapDatabaseExtensionDependenciesKey = @"extensionDependencies";
|
|
NSString *const YapDatabaseNotificationKey = @"notification";
|
|
|
|
/**
|
|
* ConnectionPool value dictionary keys.
|
|
**/
|
|
|
|
static NSString *const YDBConnectionPoolValueKey_db = @"db";
|
|
static NSString *const YDBConnectionPoolValueKey_main_file = @"main_file";
|
|
static NSString *const YDBConnectionPoolValueKey_wal_file = @"wal_file";
|
|
|
|
/**
|
|
* The database version is stored (via pragma user_version) to sqlite.
|
|
* It is used to represent the version of the userlying architecture of YapDatabase.
|
|
* In the event of future changes to the sqlite underpinnings of YapDatabase,
|
|
* the version can be consulted to allow for proper on-the-fly upgrades.
|
|
* For more information, see the upgradeTable method.
|
|
**/
|
|
#define YAP_DATABASE_CURRENT_VERION 3
|
|
|
|
/**
|
|
* Default values
|
|
**/
|
|
#define DEFAULT_MAX_CONNECTION_POOL_COUNT 5 // connections
|
|
#define DEFAULT_CONNECTION_POOL_LIFETIME 90.0 // seconds
|
|
|
|
|
|
static int connectionBusyHandler(void *ptr, int count) {
|
|
YapDatabase* currentDatabase = (__bridge YapDatabase*)ptr;
|
|
|
|
usleep(50*1000); // sleep 50ms
|
|
|
|
if (count % 4 == 1) { // log every 4th attempt but not the first one
|
|
YDBLogWarn(@"Cannot obtain busy lock on SQLite from database (%p), is another process locking the database? Retrying in 50ms...", currentDatabase);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
@implementation YapDatabase {
|
|
@private
|
|
|
|
YapDatabaseOptions *options;
|
|
|
|
sqlite3 *db; // Used for setup & checkpoints
|
|
|
|
NSMutableArray *changesets;
|
|
uint64_t snapshot;
|
|
|
|
dispatch_queue_t internalQueue;
|
|
dispatch_queue_t checkpointQueue;
|
|
|
|
YapDatabaseConnectionConfig *connectionDefaults;
|
|
|
|
NSDictionary *registeredExtensions;
|
|
NSDictionary *registeredMemoryTables;
|
|
|
|
NSArray *extensionsOrder;
|
|
NSDictionary *extensionDependencies;
|
|
|
|
YapDatabaseConnection *registrationConnection;
|
|
|
|
NSUInteger maxConnectionPoolCount;
|
|
NSTimeInterval connectionPoolLifetime;
|
|
dispatch_source_t connectionPoolTimer;
|
|
NSMutableArray *connectionPoolValues;
|
|
NSMutableArray *connectionPoolDates;
|
|
|
|
NSString *sqliteVersion;
|
|
uint64_t pageSize;
|
|
|
|
atomic_flag pendingPassiveCheckpoint;
|
|
atomic_flag pendingAggressiveCheckpoint;
|
|
atomic_bool aggressiveCheckpointEnabled;
|
|
}
|
|
|
|
/**
|
|
* The default serializer & deserializer use NSCoding (NSKeyedArchiver & NSKeyedUnarchiver).
|
|
* Thus the objects need only support the NSCoding protocol.
|
|
**/
|
|
+ (YapDatabaseSerializer)defaultSerializer
|
|
{
|
|
return ^ NSData* (NSString __unused *collection, NSString __unused *key, id object){
|
|
return [NSKeyedArchiver archivedDataWithRootObject:object];
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The default serializer & deserializer use NSCoding (NSKeyedArchiver & NSKeyedUnarchiver).
|
|
* Thus the objects need only support the NSCoding protocol.
|
|
**/
|
|
+ (YapDatabaseDeserializer)defaultDeserializer
|
|
{
|
|
return ^ id (NSString __unused *collection, NSString __unused *key, NSData *data){
|
|
return data && data.length > 0 ? [NSKeyedUnarchiver unarchiveObjectWithData:data] : nil;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Property lists ONLY support the following: NSData, NSString, NSArray, NSDictionary, NSDate, and NSNumber.
|
|
* Property lists are highly optimized and are used extensively by Apple.
|
|
*
|
|
* Property lists make a good fit when your existing code already uses them,
|
|
* such as replacing NSUserDefaults with a database.
|
|
**/
|
|
+ (YapDatabaseSerializer)propertyListSerializer
|
|
{
|
|
return ^ NSData* (NSString __unused *collection, NSString __unused *key, id object){
|
|
return [NSPropertyListSerialization dataWithPropertyList:object
|
|
format:NSPropertyListBinaryFormat_v1_0
|
|
options:NSPropertyListImmutable
|
|
error:NULL];
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Property lists ONLY support the following: NSData, NSString, NSArray, NSDictionary, NSDate, and NSNumber.
|
|
* Property lists are highly optimized and are used extensively by Apple.
|
|
*
|
|
* Property lists make a good fit when your existing code already uses them,
|
|
* such as replacing NSUserDefaults with a database.
|
|
**/
|
|
+ (YapDatabaseDeserializer)propertyListDeserializer
|
|
{
|
|
return ^ id (NSString __unused *collection, NSString __unused *key, NSData *data){
|
|
return [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL];
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A FASTER serializer than the default, if serializing ONLY a NSDate object.
|
|
* You may want to use timestampSerializer & timestampDeserializer if your metadata is simply an NSDate.
|
|
**/
|
|
+ (YapDatabaseSerializer)timestampSerializer
|
|
{
|
|
return ^ NSData* (NSString __unused *collection, NSString __unused *key, id object) {
|
|
|
|
if ([object isKindOfClass:[NSDate class]])
|
|
{
|
|
NSTimeInterval timestamp = [(NSDate *)object timeIntervalSinceReferenceDate];
|
|
|
|
return [[NSData alloc] initWithBytes:(void *)×tamp length:sizeof(NSTimeInterval)];
|
|
}
|
|
else
|
|
{
|
|
return [NSKeyedArchiver archivedDataWithRootObject:object];
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A FASTER deserializer than the default, if deserializing data from timestampSerializer.
|
|
* You may want to use timestampSerializer & timestampDeserializer if your metadata is simply an NSDate.
|
|
**/
|
|
+ (YapDatabaseDeserializer)timestampDeserializer
|
|
{
|
|
return ^ id (NSString __unused *collection, NSString __unused *key, NSData *data) {
|
|
|
|
if ([data length] == sizeof(NSTimeInterval))
|
|
{
|
|
NSTimeInterval timestamp;
|
|
memcpy((void *)×tamp, [data bytes], sizeof(NSTimeInterval));
|
|
|
|
return [[NSDate alloc] initWithTimeIntervalSinceReferenceDate:timestamp];
|
|
}
|
|
else
|
|
{
|
|
return [NSKeyedUnarchiver unarchiveObjectWithData:data];
|
|
}
|
|
};
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Properties
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
@synthesize databasePath = databasePath;
|
|
@dynamic databasePath_wal;
|
|
@dynamic databasePath_shm;
|
|
|
|
@synthesize objectSerializer = objectSerializer;
|
|
@synthesize objectDeserializer = objectDeserializer;
|
|
|
|
@synthesize metadataSerializer = metadataSerializer;
|
|
@synthesize metadataDeserializer = metadataDeserializer;
|
|
|
|
@synthesize objectPreSanitizer = objectPreSanitizer;
|
|
@synthesize objectPostSanitizer = objectPostSanitizer;
|
|
|
|
@synthesize metadataPreSanitizer = metadataPreSanitizer;
|
|
@synthesize metadataPostSanitizer = metadataPostSanitizer;
|
|
|
|
@dynamic options;
|
|
@dynamic sqliteVersion;
|
|
|
|
- (NSString *)databasePath_wal
|
|
{
|
|
return [databasePath stringByAppendingString:@"-wal"];
|
|
}
|
|
|
|
- (NSString *)databasePath_shm
|
|
{
|
|
return [databasePath stringByAppendingString:@"-shm"];
|
|
}
|
|
|
|
- (YapDatabaseOptions *)options
|
|
{
|
|
return [options copy];
|
|
}
|
|
|
|
- (NSString *)sqliteVersion
|
|
{
|
|
__block NSString *result = nil;
|
|
|
|
dispatch_sync(snapshotQueue, ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
result = sqliteVersion;
|
|
|
|
#pragma clang diagnostic pop
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Init
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
- (id)initWithPath:(NSString *)inPath
|
|
{
|
|
return [self initWithPath:inPath
|
|
objectSerializer:NULL
|
|
objectDeserializer:NULL
|
|
metadataSerializer:NULL
|
|
metadataDeserializer:NULL
|
|
objectPreSanitizer:NULL
|
|
objectPostSanitizer:NULL
|
|
metadataPreSanitizer:NULL
|
|
metadataPostSanitizer:NULL
|
|
options:nil];
|
|
}
|
|
|
|
- (id)initWithPath:(NSString *)inPath
|
|
options:(nullable YapDatabaseOptions *)inOptions
|
|
{
|
|
return [self initWithPath:inPath
|
|
objectSerializer:NULL
|
|
objectDeserializer:NULL
|
|
metadataSerializer:NULL
|
|
metadataDeserializer:NULL
|
|
objectPreSanitizer:NULL
|
|
objectPostSanitizer:NULL
|
|
metadataPreSanitizer:NULL
|
|
metadataPostSanitizer:NULL
|
|
options:inOptions];
|
|
}
|
|
|
|
- (id)initWithPath:(NSString *)inPath
|
|
serializer:(YapDatabaseSerializer)inSerializer
|
|
deserializer:(YapDatabaseDeserializer)inDeserializer
|
|
{
|
|
return [self initWithPath:inPath
|
|
objectSerializer:inSerializer
|
|
objectDeserializer:inDeserializer
|
|
metadataSerializer:inSerializer
|
|
metadataDeserializer:inDeserializer
|
|
objectPreSanitizer:NULL
|
|
objectPostSanitizer:NULL
|
|
metadataPreSanitizer:NULL
|
|
metadataPostSanitizer:NULL
|
|
options:nil];
|
|
}
|
|
|
|
- (id)initWithPath:(NSString *)inPath
|
|
serializer:(YapDatabaseSerializer)inSerializer
|
|
deserializer:(YapDatabaseDeserializer)inDeserializer
|
|
options:(YapDatabaseOptions *)inOptions
|
|
{
|
|
return [self initWithPath:inPath
|
|
objectSerializer:inSerializer
|
|
objectDeserializer:inDeserializer
|
|
metadataSerializer:inSerializer
|
|
metadataDeserializer:inDeserializer
|
|
objectPreSanitizer:NULL
|
|
objectPostSanitizer:NULL
|
|
metadataPreSanitizer:NULL
|
|
metadataPostSanitizer:NULL
|
|
options:inOptions];
|
|
}
|
|
|
|
- (id)initWithPath:(NSString *)inPath
|
|
serializer:(YapDatabaseSerializer)inSerializer
|
|
deserializer:(YapDatabaseDeserializer)inDeserializer
|
|
preSanitizer:(YapDatabasePreSanitizer)inPreSanitizer
|
|
postSanitizer:(YapDatabasePostSanitizer)inPostSanitizer
|
|
options:(YapDatabaseOptions *)inOptions
|
|
{
|
|
return [self initWithPath:inPath
|
|
objectSerializer:inSerializer
|
|
objectDeserializer:inDeserializer
|
|
metadataSerializer:inSerializer
|
|
metadataDeserializer:inDeserializer
|
|
objectPreSanitizer:inPreSanitizer
|
|
objectPostSanitizer:inPostSanitizer
|
|
metadataPreSanitizer:inPreSanitizer
|
|
metadataPostSanitizer:inPostSanitizer
|
|
options:inOptions];
|
|
}
|
|
|
|
- (id)initWithPath:(NSString *)inPath objectSerializer:(YapDatabaseSerializer)inObjectSerializer
|
|
objectDeserializer:(YapDatabaseDeserializer)inObjectDeserializer
|
|
metadataSerializer:(YapDatabaseSerializer)inMetadataSerializer
|
|
metadataDeserializer:(YapDatabaseDeserializer)inMetadataDeserializer
|
|
{
|
|
return [self initWithPath:inPath
|
|
objectSerializer:inObjectSerializer
|
|
objectDeserializer:inObjectDeserializer
|
|
metadataSerializer:inMetadataSerializer
|
|
metadataDeserializer:inMetadataDeserializer
|
|
objectPreSanitizer:NULL
|
|
objectPostSanitizer:NULL
|
|
metadataPreSanitizer:NULL
|
|
metadataPostSanitizer:NULL
|
|
options:nil];
|
|
}
|
|
|
|
- (id)initWithPath:(NSString *)inPath objectSerializer:(YapDatabaseSerializer)inObjectSerializer
|
|
objectDeserializer:(YapDatabaseDeserializer)inObjectDeserializer
|
|
metadataSerializer:(YapDatabaseSerializer)inMetadataSerializer
|
|
metadataDeserializer:(YapDatabaseDeserializer)inMetadataDeserializer
|
|
options:(YapDatabaseOptions *)inOptions
|
|
{
|
|
return [self initWithPath:inPath
|
|
objectSerializer:inObjectSerializer
|
|
objectDeserializer:inObjectDeserializer
|
|
metadataSerializer:inMetadataSerializer
|
|
metadataDeserializer:inMetadataDeserializer
|
|
objectPreSanitizer:NULL
|
|
objectPostSanitizer:NULL
|
|
metadataPreSanitizer:NULL
|
|
metadataPostSanitizer:NULL
|
|
options:inOptions];
|
|
}
|
|
|
|
- (id)initWithPath:(NSString *)inPath objectSerializer:(YapDatabaseSerializer)inObjectSerializer
|
|
objectDeserializer:(YapDatabaseDeserializer)inObjectDeserializer
|
|
metadataSerializer:(YapDatabaseSerializer)inMetadataSerializer
|
|
metadataDeserializer:(YapDatabaseDeserializer)inMetadataDeserializer
|
|
objectPreSanitizer:(YapDatabasePreSanitizer)inObjectPreSanitizer
|
|
objectPostSanitizer:(YapDatabasePostSanitizer)inObjectPostSanitizer
|
|
metadataPreSanitizer:(YapDatabasePreSanitizer)inMetadataPreSanitizer
|
|
metadataPostSanitizer:(YapDatabasePostSanitizer)inMetadataPostSanitizer
|
|
options:(YapDatabaseOptions *)inOptions
|
|
{
|
|
// First, standardize path.
|
|
// This allows clients to be lazy when passing paths.
|
|
NSString *path = [inPath stringByStandardizingPath];
|
|
|
|
// Ensure there is only a single database instance per file.
|
|
// However, clients may create as many connections as desired.
|
|
if (![YapDatabaseManager registerDatabaseForPath:path])
|
|
{
|
|
YDBLogError(@"Only a single database instance is allowed per file. "
|
|
@"For concurrency you create multiple connections from a single database instance.");
|
|
return nil;
|
|
}
|
|
|
|
if ((self = [super init]))
|
|
{
|
|
databasePath = path;
|
|
options = inOptions ? [inOptions copy] : [[YapDatabaseOptions alloc] init];
|
|
|
|
__block BOOL isNewDatabaseFile = ![[NSFileManager defaultManager] fileExistsAtPath:databasePath];
|
|
|
|
BOOL(^openConfigCreate)(void) = ^BOOL (void) { @autoreleasepool {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
BOOL result = YES;
|
|
|
|
if (result) result = [self openDatabase];
|
|
#ifdef SQLITE_HAS_CODEC
|
|
if (result) result = [self configureEncryptionForDatabase:db];
|
|
#endif
|
|
if (result) result = [self configureDatabase:isNewDatabaseFile];
|
|
if (result) result = [self createTables];
|
|
|
|
if (!result && db)
|
|
{
|
|
sqlite3_close(db);
|
|
db = NULL;
|
|
}
|
|
|
|
return result;
|
|
|
|
#pragma clang diagnostic pop
|
|
}};
|
|
|
|
BOOL result = openConfigCreate();
|
|
if (!result)
|
|
{
|
|
// There are a few reasons why the database might not open.
|
|
// One possibility is if the database file has become corrupt.
|
|
|
|
if (options.corruptAction == YapDatabaseCorruptAction_Fail)
|
|
{
|
|
// Fail - do not try to resolve
|
|
}
|
|
else if (options.corruptAction == YapDatabaseCorruptAction_Rename)
|
|
{
|
|
// Try to rename the corrupt database file.
|
|
|
|
BOOL renamed = NO;
|
|
BOOL failed = NO;
|
|
|
|
NSString *newDatabasePath = nil;
|
|
int i = 0;
|
|
|
|
do
|
|
{
|
|
NSString *extension = [NSString stringWithFormat:@"%d.corrupt", i];
|
|
newDatabasePath = [databasePath stringByAppendingPathExtension:extension];
|
|
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:newDatabasePath])
|
|
{
|
|
i++;
|
|
}
|
|
else
|
|
{
|
|
NSError *error = nil;
|
|
renamed = [[NSFileManager defaultManager] moveItemAtPath:databasePath
|
|
toPath:newDatabasePath
|
|
error:&error];
|
|
if (!renamed)
|
|
{
|
|
failed = YES;
|
|
YDBLogError(@"Error renaming corrupt database file: (%@ -> %@) %@",
|
|
[databasePath lastPathComponent], [newDatabasePath lastPathComponent], error);
|
|
}
|
|
}
|
|
|
|
} while (i < INT_MAX && !renamed && !failed);
|
|
|
|
if (renamed)
|
|
{
|
|
isNewDatabaseFile = YES;
|
|
result = openConfigCreate();
|
|
if (result) {
|
|
YDBLogInfo(@"Database corruption resolved. Renamed corrupt file. (newDB=%@) (corruptDB=%@)",
|
|
[databasePath lastPathComponent], [newDatabasePath lastPathComponent]);
|
|
}
|
|
else {
|
|
YDBLogError(@"Database corruption unresolved. (name=%@)", [databasePath lastPathComponent]);
|
|
}
|
|
}
|
|
|
|
}
|
|
else // if (options.corruptAction == YapDatabaseCorruptAction_Delete)
|
|
{
|
|
// Try to delete the corrupt database file.
|
|
|
|
NSError *error = nil;
|
|
BOOL deleted = [[NSFileManager defaultManager] removeItemAtPath:path error:&error];
|
|
|
|
if (deleted)
|
|
{
|
|
isNewDatabaseFile = YES;
|
|
result = openConfigCreate();
|
|
if (result) {
|
|
YDBLogInfo(@"Database corruption resolved. Deleted corrupt file. (name=%@)",
|
|
[databasePath lastPathComponent]);
|
|
}
|
|
else {
|
|
YDBLogError(@"Database corruption unresolved. (name=%@)", [databasePath lastPathComponent]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
YDBLogError(@"Error deleting corrupt database file: %@", error);
|
|
}
|
|
}
|
|
}
|
|
if (!result)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
// Configure VFS shim (for database connections).
|
|
|
|
yap_vfs_shim_name = [NSString stringWithFormat:@"yap_vfs_shim_%@", [[NSUUID UUID] UUIDString]];
|
|
yap_vfs_shim_register([yap_vfs_shim_name UTF8String], NULL, &yap_vfs_shim);
|
|
|
|
// Initialize variables
|
|
|
|
internalQueue = dispatch_queue_create("YapDatabase-Internal", NULL);
|
|
checkpointQueue = dispatch_queue_create("YapDatabase-Checkpoint", NULL);
|
|
snapshotQueue = dispatch_queue_create("YapDatabase-Snapshot", NULL);
|
|
writeQueue = dispatch_queue_create("YapDatabase-Write", NULL);
|
|
|
|
changesets = [[NSMutableArray alloc] init];
|
|
connectionStates = [[NSMutableArray alloc] init];
|
|
|
|
connectionDefaults = [[YapDatabaseConnectionConfig alloc] init];
|
|
|
|
registeredExtensions = [[NSDictionary alloc] init];
|
|
registeredMemoryTables = [[NSDictionary alloc] init];
|
|
|
|
extensionDependencies = [[NSDictionary alloc] init];
|
|
extensionsOrder = [[NSArray alloc] init];
|
|
|
|
maxConnectionPoolCount = DEFAULT_MAX_CONNECTION_POOL_COUNT;
|
|
connectionPoolLifetime = DEFAULT_CONNECTION_POOL_LIFETIME;
|
|
|
|
YapDatabaseSerializer defaultSerializer = nil;
|
|
YapDatabaseDeserializer defaultDeserializer = nil;
|
|
|
|
if (!inObjectSerializer || !inMetadataSerializer)
|
|
defaultSerializer = [[self class] defaultSerializer];
|
|
|
|
if (!inObjectDeserializer || !inMetadataDeserializer)
|
|
defaultDeserializer = [[self class] defaultDeserializer];
|
|
|
|
objectSerializer = (YapDatabaseSerializer)[inObjectSerializer copy] ?: defaultSerializer;
|
|
objectDeserializer = (YapDatabaseDeserializer)[inObjectDeserializer copy] ?: defaultDeserializer;
|
|
|
|
metadataSerializer = (YapDatabaseSerializer)[inMetadataSerializer copy] ?: defaultSerializer;
|
|
metadataDeserializer = (YapDatabaseDeserializer)[inMetadataDeserializer copy] ?: defaultDeserializer;
|
|
|
|
objectPreSanitizer = (YapDatabasePreSanitizer)[inObjectPreSanitizer copy];
|
|
objectPostSanitizer = (YapDatabasePostSanitizer)[inObjectPostSanitizer copy];
|
|
|
|
metadataPreSanitizer = (YapDatabasePreSanitizer)[inMetadataPreSanitizer copy];
|
|
metadataPostSanitizer = (YapDatabasePostSanitizer)[inMetadataPostSanitizer copy];
|
|
|
|
// Mark the queues so we can identify them.
|
|
// There are several methods whose use is restricted to within a certain queue.
|
|
|
|
IsOnSnapshotQueueKey = &IsOnSnapshotQueueKey;
|
|
dispatch_queue_set_specific(snapshotQueue, IsOnSnapshotQueueKey, IsOnSnapshotQueueKey, NULL);
|
|
|
|
IsOnWriteQueueKey = &IsOnWriteQueueKey;
|
|
dispatch_queue_set_specific(writeQueue, IsOnWriteQueueKey, IsOnWriteQueueKey, NULL);
|
|
|
|
// Complete database setup in the background
|
|
|
|
dispatch_async(snapshotQueue, ^{ @autoreleasepool {
|
|
|
|
[self upgradeTable];
|
|
[self prepare];
|
|
}});
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
YDBLogVerbose(@"Dealloc <%@ %p: databaseName=%@>", [self class], self, [databasePath lastPathComponent]);
|
|
|
|
NSDictionary *userInfo = @{
|
|
YapDatabasePathKey : self.databasePath ?: @"",
|
|
YapDatabasePathWalKey : self.databasePath_wal ?: @"",
|
|
YapDatabasePathShmKey : self.databasePath_shm ?: @""
|
|
};
|
|
NSNotification *notification =
|
|
[NSNotification notificationWithName:YapDatabaseClosedNotification
|
|
object:nil // Cannot retain self within dealloc method
|
|
userInfo:userInfo];
|
|
|
|
while ([connectionPoolValues count] > 0)
|
|
{
|
|
NSDictionary *value = [connectionPoolValues objectAtIndex:0];
|
|
|
|
sqlite3 *aDb = (sqlite3 *)[[value objectForKey:YDBConnectionPoolValueKey_db] pointerValue];
|
|
|
|
int status = sqlite3_close(aDb);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Error in sqlite_close: %d %s", status, sqlite3_errmsg(aDb));
|
|
}
|
|
|
|
[connectionPoolValues removeObjectAtIndex:0];
|
|
[connectionPoolDates removeObjectAtIndex:0];
|
|
}
|
|
|
|
if (connectionPoolTimer)
|
|
dispatch_source_cancel(connectionPoolTimer);
|
|
|
|
if (db) {
|
|
sqlite3_close(db);
|
|
db = NULL;
|
|
}
|
|
if (yap_vfs_shim) {
|
|
yap_vfs_shim_unregister(&yap_vfs_shim);
|
|
}
|
|
|
|
[YapDatabaseManager deregisterDatabaseForPath:databasePath];
|
|
|
|
#if !OS_OBJECT_USE_OBJC
|
|
if (internalQueue)
|
|
dispatch_release(internalQueue);
|
|
if (snapshotQueue)
|
|
dispatch_release(snapshotQueue);
|
|
if (writeQueue)
|
|
dispatch_release(writeQueue);
|
|
if (checkpointQueue)
|
|
dispatch_release(checkpointQueue);
|
|
#endif
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
[[NSNotificationCenter defaultCenter] postNotification:notification];
|
|
});
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Setup
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Attempts to open (or create & open) the database connection.
|
|
**/
|
|
- (BOOL)openDatabase
|
|
{
|
|
// Open the database connection.
|
|
//
|
|
// We use SQLITE_OPEN_NOMUTEX to use the multi-thread threading mode,
|
|
// as we will be serializing access to the connection externally.
|
|
|
|
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_PRIVATECACHE;
|
|
|
|
int status = sqlite3_open_v2([databasePath UTF8String], &db, flags, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
// There are a few reasons why the database might not open.
|
|
// One possibility is if the database file has become corrupt.
|
|
|
|
// Sometimes the open function returns a db to allow us to query it for the error message.
|
|
// The openConfigCreate block will close it for us.
|
|
if (db) {
|
|
YDBLogError(@"Error opening database: %d %s", status, sqlite3_errmsg(db));
|
|
}
|
|
else {
|
|
YDBLogError(@"Error opening database: %d", status);
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
// Add a busy handler if we are in multiprocess mode
|
|
if (options.enableMultiProcessSupport) {
|
|
sqlite3_busy_handler(db, connectionBusyHandler, (__bridge void *)(self));
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
/**
|
|
* Configures the database connection.
|
|
* This mainly means enabling WAL mode, and configuring the auto-checkpoint.
|
|
**/
|
|
- (BOOL)configureDatabase:(BOOL)isNewDatabaseFile
|
|
{
|
|
int status;
|
|
|
|
// Set mandatory pragmas
|
|
|
|
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)
|
|
{
|
|
YDBLogError(@"Error setting PRAGMA journal_mode: %d %s", status, sqlite3_errmsg(db));
|
|
return NO;
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
YDBLogError(@"Error setting PRAGMA synchronous: %d %s", status, sqlite3_errmsg(db));
|
|
// This isn't critical, so we can continue.
|
|
}
|
|
|
|
// Set journal_size_imit.
|
|
//
|
|
// We only need to do set this pragma for THIS connection,
|
|
// because it is the only connection that performs checkpoints.
|
|
|
|
NSString *pragma_journal_size_limit =
|
|
[NSString stringWithFormat:@"PRAGMA journal_size_limit = %ld;", (long)options.pragmaJournalSizeLimit];
|
|
|
|
status = sqlite3_exec(db, [pragma_journal_size_limit UTF8String], NULL, NULL, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"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.
|
|
|
|
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);
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
#ifdef SQLITE_HAS_CODEC
|
|
/**
|
|
* Configures database encryption via SQLCipher.
|
|
**/
|
|
- (BOOL)configureEncryptionForDatabase:(sqlite3 *)sqlite
|
|
{
|
|
if (options.cipherKeyBlock ||
|
|
options.cipherKeySpecBlock)
|
|
{
|
|
NSData *_Nullable keyData = nil;
|
|
if (options.cipherKeySpecBlock)
|
|
{
|
|
keyData = options.cipherKeySpecBlock();
|
|
if (!keyData)
|
|
{
|
|
NSAssert(NO, @"YapDatabaseOptions.cipherKeySpecBlock cannot return nil!");
|
|
return NO;
|
|
}
|
|
} else {
|
|
keyData = options.cipherKeyBlock();
|
|
if (!keyData)
|
|
{
|
|
NSAssert(NO, @"YapDatabaseOptions.cipherKeyBlock cannot return nil!");
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
//Setting the PBKDF2 default iteration number (this will have effect next time database is opened)
|
|
if (options.cipherDefaultkdfIterNumber > 0) {
|
|
char *errorMsg;
|
|
NSString *pragmaCommand = [NSString stringWithFormat:@"PRAGMA cipher_default_kdf_iter = %lu", (unsigned long)options.cipherDefaultkdfIterNumber];
|
|
if (sqlite3_exec(sqlite, [pragmaCommand UTF8String], NULL, NULL, &errorMsg) != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"failed to set database cipher_default_kdf_iter: %s", errorMsg);
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
//Setting the PBKDF2 iteration number
|
|
if (options.kdfIterNumber > 0) {
|
|
char *errorMsg;
|
|
NSString *pragmaCommand = [NSString stringWithFormat:@"PRAGMA kdf_iter = %lu", (unsigned long)options.kdfIterNumber];
|
|
if (sqlite3_exec(sqlite, [pragmaCommand UTF8String], NULL, NULL, &errorMsg) != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"failed to set database kdf_iter: %s", errorMsg);
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
//Setting the encrypted database page size
|
|
if (options.cipherPageSize > 0) {
|
|
char *errorMsg;
|
|
NSString *pragmaCommand = [NSString stringWithFormat:@"PRAGMA cipher_page_size = %lu", (unsigned long)options.cipherPageSize];
|
|
if (sqlite3_exec(sqlite, [pragmaCommand UTF8String], NULL, NULL, &errorMsg) != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"failed to set database cipher_page_size: %s", errorMsg);
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
if (options.cipherKeySpecBlock) {
|
|
// Use a raw key spec, where the 96 hexadecimal digits are provided
|
|
// (i.e. 64 hex for the 256 bit key, followed by 32 hex for the 128 bit salt)
|
|
// using explicit BLOB syntax, e.g.:
|
|
//
|
|
// x'98483C6EB40B6C31A448C22A66DED3B5E5E8D5119CAC8327B655C8B5C483648101010101010101010101010101010101'
|
|
NSString *keySpecString = [NSString stringWithFormat:@"x'%@'", [self hexadecimalStringForData:keyData]];
|
|
NSData *keySpecStringData = [keySpecString dataUsingEncoding:NSUTF8StringEncoding];
|
|
int status = sqlite3_key(sqlite, [keySpecStringData bytes], (int)[keySpecStringData length]);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Error setting SQLCipher key: %d %s", status, sqlite3_errmsg(sqlite));
|
|
return NO;
|
|
}
|
|
} else {
|
|
int status = sqlite3_key(sqlite, [keyData bytes], (int)[keyData length]);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Error setting SQLCipher key: %d %s", status, sqlite3_errmsg(sqlite));
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
if (options.cipherUnencryptedHeaderLength > 0 &&
|
|
(options.cipherKeySpecBlock ||
|
|
options.cipherSaltBlock)) {
|
|
|
|
if (options.cipherKeySpecBlock) {
|
|
// YapDatabase using cipher key spec and unencrypted header.
|
|
} else {
|
|
// YapDatabase using cipher salt and unencrypted header.
|
|
|
|
NSData *_Nullable saltData = options.cipherSaltBlock();
|
|
|
|
if (saltData == nil)
|
|
{
|
|
NSAssert(NO, @"YapDatabaseOptions.cipherSaltBlock cannot return nil!");
|
|
return NO;
|
|
}
|
|
|
|
{
|
|
char *errorMsg;
|
|
// Example: PRAGMA cipher_salt = "x'01010101010101010101010101010101';";
|
|
NSString *pragmaSql = [NSString stringWithFormat:@"PRAGMA cipher_salt = \"x'%@'\";", [self hexadecimalStringForData:saltData]];
|
|
if (sqlite3_exec(sqlite, [pragmaSql UTF8String], NULL, NULL, &errorMsg) != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"failed to set database cipher_default_kdf_iter: %s", errorMsg);
|
|
return NO;
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
// We use cipher_plaintext_header_size NOT cipher_default_plaintext_header_size,
|
|
// since the _default_ pragma affects a static variable.
|
|
NSString *pragmaSql =
|
|
[NSString stringWithFormat:@"PRAGMA cipher_plaintext_header_size = %zd;", options.cipherUnencryptedHeaderLength];
|
|
int status = sqlite3_exec(sqlite, [pragmaSql UTF8String], NULL, NULL, NULL);
|
|
if (status != SQLITE_OK) {
|
|
YDBLogError(@"Error setting PRAGMA cipher_plaintext_header_size = %zd: status: %d, error: %s",
|
|
options.cipherUnencryptedHeaderLength,
|
|
status,
|
|
sqlite3_errmsg(sqlite));
|
|
return NO;
|
|
}
|
|
}
|
|
} else {
|
|
if (options.cipherUnencryptedHeaderLength > 0) {
|
|
NSAssert(NO, @"YapDatabaseOptions.cipherUnencryptedHeaderLength should not be used without cipherKeySpecBlock or cipherSaltBlock!");
|
|
return NO;
|
|
}
|
|
if (options.cipherKeySpecBlock) {
|
|
NSAssert(NO, @"YapDatabaseOptions.cipherKeySpecBlock should not be used without setting cipherUnencryptedHeaderLength!");
|
|
return NO;
|
|
}
|
|
if (options.cipherSaltBlock) {
|
|
NSAssert(NO, @"YapDatabaseOptions.cipherSaltBlock should not be used without setting cipherUnencryptedHeaderLength!");
|
|
return NO;
|
|
}
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (NSString *)hexadecimalStringForData:(NSData *)data {
|
|
/* Returns hexadecimal string of NSData. Empty string if data is empty. */
|
|
const unsigned char *dataBuffer = (const unsigned char *)[data bytes];
|
|
if (!dataBuffer) {
|
|
return @"";
|
|
}
|
|
|
|
NSUInteger dataLength = [data length];
|
|
NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)];
|
|
|
|
for (NSUInteger i = 0; i < dataLength; ++i) {
|
|
[hexString appendFormat:@"%02x", dataBuffer[i]];
|
|
}
|
|
return [hexString copy];
|
|
}
|
|
|
|
#endif
|
|
|
|
/**
|
|
* Creates the database tables we need:
|
|
*
|
|
* - yap2 : stores snapshot and metadata for extensions
|
|
* - database2 : stores collection/key/value/metadata rows
|
|
**/
|
|
- (BOOL)createTables
|
|
{
|
|
int status;
|
|
|
|
char *createYapTableStatement =
|
|
"CREATE TABLE IF NOT EXISTS \"yap2\""
|
|
" (\"extension\" CHAR NOT NULL, "
|
|
" \"key\" CHAR NOT NULL, "
|
|
" \"data\" BLOB, "
|
|
" PRIMARY KEY (\"extension\", \"key\")"
|
|
" );";
|
|
|
|
status = sqlite3_exec(db, createYapTableStatement, NULL, NULL, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Failed creating 'yap2' table: %d %s", status, sqlite3_errmsg(db));
|
|
return NO;
|
|
}
|
|
|
|
char *createDatabaseTableStatement =
|
|
"CREATE TABLE IF NOT EXISTS \"database2\""
|
|
" (\"rowid\" INTEGER PRIMARY KEY,"
|
|
" \"collection\" CHAR NOT NULL,"
|
|
" \"key\" CHAR NOT NULL,"
|
|
" \"data\" BLOB,"
|
|
" \"metadata\" BLOB"
|
|
" );";
|
|
|
|
status = sqlite3_exec(db, createDatabaseTableStatement, NULL, NULL, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Failed creating 'database2' table: %d %s", status, sqlite3_errmsg(db));
|
|
return NO;
|
|
}
|
|
|
|
char *createIndexStatement =
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS \"true_primary_key\" ON \"database2\" ( \"collection\", \"key\" );";
|
|
|
|
status = sqlite3_exec(db, createIndexStatement, NULL, NULL, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Failed creating index on 'database2' table: %d %s", status, sqlite3_errmsg(db));
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Utilities
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
+ (NSString *)sqliteVersionUsing:(sqlite3 *)aDb
|
|
{
|
|
sqlite3_stmt *statement;
|
|
|
|
int status = sqlite3_prepare_v2(aDb, "SELECT sqlite_version();", -1, &statement, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"%@: Error creating statement! %d %s", THIS_METHOD, status, sqlite3_errmsg(aDb));
|
|
return nil;
|
|
}
|
|
|
|
NSString *version = nil;
|
|
|
|
status = sqlite3_step(statement);
|
|
if (status == SQLITE_ROW)
|
|
{
|
|
const unsigned char *text = sqlite3_column_text(statement, SQLITE_COLUMN_START);
|
|
int textSize = sqlite3_column_bytes(statement, SQLITE_COLUMN_START);
|
|
|
|
version = [[NSString alloc] initWithBytes:text length:textSize encoding:NSUTF8StringEncoding];
|
|
}
|
|
else
|
|
{
|
|
YDBLogError(@"%@: Error executing statement! %d %s", THIS_METHOD, status, sqlite3_errmsg(aDb));
|
|
}
|
|
|
|
sqlite3_finalize(statement);
|
|
statement = NULL;
|
|
|
|
return version;
|
|
}
|
|
|
|
+ (int64_t)pragma:(NSString *)pragmaSetting using:(sqlite3 *)aDb
|
|
{
|
|
if (pragmaSetting == nil) return -1;
|
|
|
|
sqlite3_stmt *statement;
|
|
NSString *pragma = [NSString stringWithFormat:@"PRAGMA %@;", pragmaSetting];
|
|
|
|
int status = sqlite3_prepare_v2(aDb, [pragma UTF8String], -1, &statement, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"%@: Error creating statement! %d %s", THIS_METHOD, status, sqlite3_errmsg(aDb));
|
|
return NO;
|
|
}
|
|
|
|
int64_t result = -1;
|
|
|
|
status = sqlite3_step(statement);
|
|
if (status == SQLITE_ROW)
|
|
{
|
|
result = sqlite3_column_int64(statement, SQLITE_COLUMN_START);
|
|
}
|
|
else if (status == SQLITE_ERROR)
|
|
{
|
|
YDBLogError(@"%@: Error executing statement! %d %s", THIS_METHOD, status, sqlite3_errmsg(aDb));
|
|
}
|
|
|
|
sqlite3_finalize(statement);
|
|
statement = NULL;
|
|
|
|
return result;
|
|
}
|
|
|
|
+ (NSString *)pragmaValueForSynchronous:(int64_t)synchronous
|
|
{
|
|
switch(synchronous)
|
|
{
|
|
case 0 : return @"OFF";
|
|
case 1 : return @"NORMAL";
|
|
case 2 : return @"FULL";
|
|
default: return @"UNKNOWN";
|
|
}
|
|
}
|
|
|
|
+ (NSString *)pragmaValueForAutoVacuum:(int64_t)auto_vacuum
|
|
{
|
|
switch(auto_vacuum)
|
|
{
|
|
case 0 : return @"NONE";
|
|
case 1 : return @"FULL";
|
|
case 2 : return @"INCREMENTAL";
|
|
default: return @"UNKNOWN";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not the given table exists.
|
|
**/
|
|
+ (BOOL)tableExists:(NSString *)tableName using:(sqlite3 *)aDb
|
|
{
|
|
if (tableName == nil) return NO;
|
|
|
|
sqlite3_stmt *statement;
|
|
char *stmt = "SELECT count(*) FROM sqlite_master WHERE type = 'table' AND name = ?";
|
|
|
|
int status = sqlite3_prepare_v2(aDb, stmt, (int)strlen(stmt)+1, &statement, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"%@: Error creating statement! %d %s", THIS_METHOD, status, sqlite3_errmsg(aDb));
|
|
return NO;
|
|
}
|
|
|
|
BOOL result = NO;
|
|
|
|
sqlite3_bind_text(statement, SQLITE_BIND_START, [tableName UTF8String], -1, SQLITE_TRANSIENT);
|
|
|
|
status = sqlite3_step(statement);
|
|
if (status == SQLITE_ROW)
|
|
{
|
|
int count = sqlite3_column_int(statement, SQLITE_COLUMN_START);
|
|
|
|
result = (count > 0);
|
|
}
|
|
else if (status == SQLITE_ERROR)
|
|
{
|
|
YDBLogError(@"%@: Error executing statement! %d %s", THIS_METHOD, status, sqlite3_errmsg(aDb));
|
|
}
|
|
|
|
sqlite3_finalize(statement);
|
|
statement = NULL;
|
|
|
|
return result;
|
|
}
|
|
|
|
+ (NSArray *)tableNamesUsing:(sqlite3 *)aDb
|
|
{
|
|
sqlite3_stmt *statement;
|
|
char *stmt = "SELECT name FROM sqlite_master WHERE type = 'table';";
|
|
|
|
int status = sqlite3_prepare_v2(aDb, stmt, (int)strlen(stmt)+1, &statement, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"%@: Error creating statement! %d %s", THIS_METHOD, status, sqlite3_errmsg(aDb));
|
|
return nil;
|
|
}
|
|
|
|
NSMutableArray *tableNames = [NSMutableArray array];
|
|
|
|
while ((status = sqlite3_step(statement)) == SQLITE_ROW)
|
|
{
|
|
const unsigned char *text = sqlite3_column_text(statement, SQLITE_COLUMN_START);
|
|
int textSize = sqlite3_column_bytes(statement, SQLITE_COLUMN_START);
|
|
|
|
NSString *tableName = [[NSString alloc] initWithBytes:text length:textSize encoding:NSUTF8StringEncoding];
|
|
|
|
if (tableName) {
|
|
[tableNames addObject:tableName];
|
|
}
|
|
|
|
}
|
|
|
|
if (status != SQLITE_DONE)
|
|
{
|
|
YDBLogError(@"%@: Error executing statement! %d %s", THIS_METHOD, status, sqlite3_errmsg(aDb));
|
|
}
|
|
|
|
sqlite3_finalize(statement);
|
|
statement = NULL;
|
|
|
|
return tableNames;
|
|
}
|
|
|
|
/**
|
|
* Extracts and returns column names from the given table in the database.
|
|
**/
|
|
+ (NSArray *)columnNamesForTable:(NSString *)tableName using:(sqlite3 *)aDb
|
|
{
|
|
if (tableName == nil) return nil;
|
|
|
|
sqlite3_stmt *statement;
|
|
NSString *pragma = [NSString stringWithFormat:@"PRAGMA table_info('%@');", tableName];
|
|
|
|
int status = sqlite3_prepare_v2(aDb, [pragma UTF8String], -1, &statement, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"%@: Error creating statement! %d %s", THIS_METHOD, status, sqlite3_errmsg(aDb));
|
|
return nil;
|
|
}
|
|
|
|
NSMutableArray *tableColumnNames = [NSMutableArray array];
|
|
|
|
while ((status = sqlite3_step(statement)) == SQLITE_ROW)
|
|
{
|
|
// cid|name|type|notnull|dflt|value|pk
|
|
// 0 |1 |2 |3 |4 |5 |6
|
|
|
|
const unsigned char *text = sqlite3_column_text(statement, 1);
|
|
int textSize = sqlite3_column_bytes(statement, 1);
|
|
|
|
NSString *columnName = [[NSString alloc] initWithBytes:text length:textSize encoding:NSUTF8StringEncoding];
|
|
if (columnName)
|
|
{
|
|
[tableColumnNames addObject:columnName];
|
|
}
|
|
}
|
|
|
|
if (status != SQLITE_DONE)
|
|
{
|
|
YDBLogError(@"%@: Error executing statement! %d %s", THIS_METHOD, status, sqlite3_errmsg(aDb));
|
|
}
|
|
|
|
sqlite3_finalize(statement);
|
|
statement = NULL;
|
|
|
|
return tableColumnNames;
|
|
}
|
|
|
|
/**
|
|
* Extracts and returns column names & affinity for the given table in the database.
|
|
* The dictionary format is:
|
|
*
|
|
* key:(NSString *)columnName -> value:(NSString *)affinity
|
|
**/
|
|
+ (NSDictionary *)columnNamesAndAffinityForTable:(NSString *)tableName using:(sqlite3 *)aDb
|
|
{
|
|
if (tableName == nil) return nil;
|
|
|
|
sqlite3_stmt *statement;
|
|
NSString *pragma = [NSString stringWithFormat:@"PRAGMA table_info('%@');", tableName];
|
|
|
|
int status = sqlite3_prepare_v2(aDb, [pragma UTF8String], -1, &statement, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"%@: Error creating statement! %d %s", THIS_METHOD, status, sqlite3_errmsg(aDb));
|
|
return nil;
|
|
}
|
|
|
|
NSMutableDictionary *columns = [NSMutableDictionary dictionary];
|
|
|
|
while ((status = sqlite3_step(statement)) == SQLITE_ROW)
|
|
{
|
|
// cid|name|type|notnull|dflt|value|pk
|
|
// 0 |1 |2 |3 |4 |5 |6
|
|
|
|
const unsigned char *_name = sqlite3_column_text(statement, 1);
|
|
int _nameSize = sqlite3_column_bytes(statement, 1);
|
|
|
|
const unsigned char *_type = sqlite3_column_text(statement, 2);
|
|
int _typeSize = sqlite3_column_bytes(statement, 2);
|
|
|
|
NSString *name = [[NSString alloc] initWithBytes:_name length:_nameSize encoding:NSUTF8StringEncoding];
|
|
NSString *affinity = [[NSString alloc] initWithBytes:_type length:_typeSize encoding:NSUTF8StringEncoding];
|
|
|
|
if (name && affinity)
|
|
{
|
|
[columns setObject:affinity forKey:name];
|
|
}
|
|
}
|
|
|
|
if (status != SQLITE_DONE)
|
|
{
|
|
YDBLogError(@"%@: Error executing statement! %d %s", THIS_METHOD, status, sqlite3_errmsg(aDb));
|
|
}
|
|
|
|
sqlite3_finalize(statement);
|
|
statement = NULL;
|
|
|
|
return columns;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Upgrade
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Gets the version of the table.
|
|
* This is used to perform the various upgrade paths.
|
|
**/
|
|
- (BOOL)get_user_version:(int *)user_version_ptr
|
|
{
|
|
sqlite3_stmt *pragmaStatement;
|
|
int status;
|
|
int user_version;
|
|
|
|
char *stmt = "PRAGMA user_version;";
|
|
|
|
status = sqlite3_prepare_v2(db, stmt, (int)strlen(stmt)+1, &pragmaStatement, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Error creating pragma user_version statement! %d %s", status, sqlite3_errmsg(db));
|
|
return NO;
|
|
}
|
|
|
|
status = sqlite3_step(pragmaStatement);
|
|
if (status == SQLITE_ROW)
|
|
{
|
|
user_version = sqlite3_column_int(pragmaStatement, SQLITE_COLUMN_START);
|
|
}
|
|
else
|
|
{
|
|
YDBLogError(@"Error fetching user_version: %d %s", status, sqlite3_errmsg(db));
|
|
return NO;
|
|
}
|
|
|
|
sqlite3_finalize(pragmaStatement);
|
|
pragmaStatement = NULL;
|
|
|
|
// If user_version is zero, then this is a new database
|
|
|
|
if (user_version == 0)
|
|
{
|
|
user_version = YAP_DATABASE_CURRENT_VERION;
|
|
[self set_user_version:user_version];
|
|
}
|
|
|
|
if (user_version_ptr)
|
|
*user_version_ptr = user_version;
|
|
return YES;
|
|
}
|
|
|
|
/**
|
|
* Sets the version of the table.
|
|
* The version is used to check and perform upgrade logic if needed.
|
|
**/
|
|
- (BOOL)set_user_version:(int)user_version
|
|
{
|
|
NSString *query = [NSString stringWithFormat:@"PRAGMA user_version = %d;", user_version];
|
|
|
|
int status = sqlite3_exec(db, [query UTF8String], NULL, NULL, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Error setting user_version: %d %s", status, sqlite3_errmsg(db));
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)upgradeTable_1_2
|
|
{
|
|
// In version 1, we used a table named "yap" which had {key, data}.
|
|
// In version 2, we use a table named "yap2" which has {extension, key, data}
|
|
|
|
int status = sqlite3_exec(db, "DROP TABLE IF EXISTS \"yap\"", NULL, NULL, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Failed dropping 'yap' table: %d %s", status, sqlite3_errmsg(db));
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
/**
|
|
* In version 3 (more commonly known as version 2.1),
|
|
* we altered the tables to use INTEGER PRIMARY KEY's so we could pass rowid's to extensions.
|
|
*
|
|
* This method migrates 'database' to 'database2'.
|
|
**/
|
|
- (BOOL)upgradeTable_2_3
|
|
{
|
|
int status;
|
|
|
|
char *stmt = "INSERT INTO \"database2\" (\"collection\", \"key\", \"data\", \"metadata\")"
|
|
" SELECT \"collection\", \"key\", \"data\", \"metadata\" FROM \"database\";";
|
|
|
|
status = sqlite3_exec(db, stmt, NULL, NULL, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Error migrating 'database' to 'database2': %d %s", status, sqlite3_errmsg(db));
|
|
return NO;
|
|
}
|
|
|
|
status = sqlite3_exec(db, "DROP TABLE IF EXISTS \"database\"", NULL, NULL, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Failed dropping 'database' table: %d %s", status, sqlite3_errmsg(db));
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
/**
|
|
* Performs upgrade checks, and implements the upgrade "plumbing" by invoking the appropriate upgrade methods.
|
|
*
|
|
* To add custom upgrade logic, implement a method named "upgradeTable_X_Y",
|
|
* where X is the previous version, and Y is the new version.
|
|
* For example:
|
|
*
|
|
* - (BOOL)upgradeTable_1_2 {
|
|
* // Upgrades from version 1 to version 2 of YapDatabase.
|
|
* // Return YES if successful.
|
|
* }
|
|
*
|
|
* IMPORTANT:
|
|
* This is for upgrades of the database schema, and low-level operations of YapDatabase.
|
|
* This is NOT for upgrading data within the database (i.e. objects, metadata, or keys).
|
|
* Such data upgrades should be performed client side.
|
|
*
|
|
* This method is run asynchronously on the queue.
|
|
**/
|
|
- (void)upgradeTable
|
|
{
|
|
int user_version = 0;
|
|
if (![self get_user_version:&user_version]) return;
|
|
|
|
while (user_version < YAP_DATABASE_CURRENT_VERION)
|
|
{
|
|
// Invoke method upgradeTable_X_Y
|
|
// where X == current_version, and Y == current_version+1.
|
|
//
|
|
// Do this until we're up-to-date.
|
|
|
|
int new_user_version = user_version + 1;
|
|
|
|
NSString *selName = [NSString stringWithFormat:@"upgradeTable_%d_%d", user_version, new_user_version];
|
|
SEL sel = NSSelectorFromString(selName);
|
|
|
|
if ([self respondsToSelector:sel])
|
|
{
|
|
YDBLogInfo(@"Upgrading database (%@) from version %d to %d...",
|
|
[databasePath lastPathComponent], user_version, new_user_version);
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
if ([self performSelector:sel])
|
|
#pragma clang diagnostic pop
|
|
{
|
|
[self set_user_version:new_user_version];
|
|
}
|
|
else
|
|
{
|
|
YDBLogError(@"Error upgrading database (%@)", [databasePath lastPathComponent]);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
YDBLogWarn(@"Missing upgrade method: %@", selName);
|
|
}
|
|
|
|
user_version = new_user_version;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Prepare
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* This method is run asynchronously on the snapshotQueue.
|
|
**/
|
|
- (void)prepare
|
|
{
|
|
// Write it to disk (replacing any previous value from last app run)
|
|
|
|
[self beginTransaction];
|
|
{
|
|
snapshot = [self readSnapshot];
|
|
|
|
sqliteVersion = [YapDatabase sqliteVersionUsing:db];
|
|
YDBLogVerbose(@"sqlite version = %@", sqliteVersion);
|
|
|
|
pageSize = (uint64_t)[YapDatabase pragma:@"page_size" using:db];
|
|
|
|
[self fetchPreviouslyRegisteredExtensionNames];
|
|
}
|
|
[self commitTransaction];
|
|
[self asyncCheckpoint:snapshot];
|
|
}
|
|
|
|
- (void)beginTransaction
|
|
{
|
|
int status = status = sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Error in '%@': %d %s", THIS_METHOD, status, sqlite3_errmsg(db));
|
|
}
|
|
}
|
|
|
|
- (void)commitTransaction
|
|
{
|
|
int status = status = sqlite3_exec(db, "COMMIT TRANSACTION;", NULL, NULL, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Error in '%@': %d %s", THIS_METHOD, status, sqlite3_errmsg(db));
|
|
}
|
|
}
|
|
|
|
- (uint64_t)readSnapshot
|
|
{
|
|
int status;
|
|
sqlite3_stmt *statement;
|
|
|
|
const char *stmt = "SELECT \"data\" FROM \"yap2\" WHERE \"extension\" = ? AND \"key\" = ?;";
|
|
|
|
int const column_idx_data = SQLITE_COLUMN_START;
|
|
int const bind_idx_extension = SQLITE_BIND_START + 0;
|
|
int const bind_idx_key = SQLITE_BIND_START + 1;
|
|
|
|
uint64_t result = 0;
|
|
|
|
status = sqlite3_prepare_v2(db, stmt, (int)strlen(stmt)+1, &statement, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"%@: Error creating statement: %d %s", THIS_METHOD, status, sqlite3_errmsg(db));
|
|
}
|
|
else
|
|
{
|
|
const char *extension = "";
|
|
sqlite3_bind_text(statement, bind_idx_extension, extension, (int)strlen(extension), SQLITE_STATIC);
|
|
|
|
const char *key = "snapshot";
|
|
sqlite3_bind_text(statement, bind_idx_key, key, (int)strlen(key), SQLITE_STATIC);
|
|
|
|
status = sqlite3_step(statement);
|
|
if (status == SQLITE_ROW)
|
|
{
|
|
result = (uint64_t)sqlite3_column_int64(statement, column_idx_data);
|
|
}
|
|
else if (status == SQLITE_ERROR)
|
|
{
|
|
YDBLogError(@"Error executing 'readSnapshot': %d %s",
|
|
status, sqlite3_errmsg(db));
|
|
}
|
|
|
|
sqlite3_finalize(statement);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
- (void)fetchPreviouslyRegisteredExtensionNames
|
|
{
|
|
int status;
|
|
sqlite3_stmt *statement;
|
|
|
|
char *stmt = "SELECT DISTINCT \"extension\" FROM \"yap2\";";
|
|
|
|
NSMutableArray *extensionNames = [NSMutableArray array];
|
|
|
|
status = sqlite3_prepare_v2(db, stmt, (int)strlen(stmt)+1, &statement, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"%@: Error creating statement: %d %s", THIS_METHOD, status, sqlite3_errmsg(db));
|
|
}
|
|
else
|
|
{
|
|
while ((status = sqlite3_step(statement)) == SQLITE_ROW)
|
|
{
|
|
const unsigned char *text = sqlite3_column_text(statement, SQLITE_COLUMN_START);
|
|
int textSize = sqlite3_column_bytes(statement, SQLITE_COLUMN_START);
|
|
|
|
NSString *extensionName =
|
|
[[NSString alloc] initWithBytes:text length:textSize encoding:NSUTF8StringEncoding];
|
|
|
|
if ([extensionName length] > 0)
|
|
{
|
|
[extensionNames addObject:extensionName];
|
|
}
|
|
}
|
|
|
|
if (status != SQLITE_DONE)
|
|
{
|
|
YDBLogError(@"%@: Error in statement: %d %s", THIS_METHOD, status, sqlite3_errmsg(db));
|
|
}
|
|
|
|
sqlite3_finalize(statement);
|
|
}
|
|
|
|
previouslyRegisteredExtensionNames = extensionNames;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Defaults
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
- (YapDatabaseConnectionConfig *)connectionDefaults
|
|
{
|
|
return connectionDefaults;
|
|
}
|
|
|
|
- (BOOL)defaultObjectCacheEnabled
|
|
{
|
|
return connectionDefaults.objectCacheEnabled;
|
|
}
|
|
|
|
- (void)setDefaultObjectCacheEnabled:(BOOL)defaultObjectCacheEnabled
|
|
{
|
|
connectionDefaults.objectCacheEnabled = defaultObjectCacheEnabled;
|
|
}
|
|
|
|
- (NSUInteger)defaultObjectCacheLimit
|
|
{
|
|
return connectionDefaults.objectCacheLimit;
|
|
}
|
|
|
|
- (void)setDefaultObjectCacheLimit:(NSUInteger)defaultObjectCacheLimit
|
|
{
|
|
connectionDefaults.objectCacheLimit = defaultObjectCacheLimit;
|
|
}
|
|
|
|
- (BOOL)defaultMetadataCacheEnabled
|
|
{
|
|
return connectionDefaults.metadataCacheEnabled;
|
|
}
|
|
|
|
- (void)setDefaultMetadataCacheEnabled:(BOOL)defaultMetadataCacheEnabled
|
|
{
|
|
connectionDefaults.metadataCacheEnabled = defaultMetadataCacheEnabled;
|
|
}
|
|
|
|
- (NSUInteger)defaultMetadataCacheLimit
|
|
{
|
|
return connectionDefaults.metadataCacheLimit;
|
|
}
|
|
|
|
- (void)setDefaultMetadataCacheLimit:(NSUInteger)defaultMetadataCacheLimit
|
|
{
|
|
connectionDefaults.metadataCacheLimit = defaultMetadataCacheLimit;
|
|
}
|
|
|
|
- (YapDatabasePolicy)defaultObjectPolicy
|
|
{
|
|
return connectionDefaults.objectPolicy;
|
|
}
|
|
|
|
- (void)setDefaultObjectPolicy:(YapDatabasePolicy)defaultObjectPolicy
|
|
{
|
|
connectionDefaults.objectPolicy = defaultObjectPolicy;
|
|
}
|
|
|
|
- (YapDatabasePolicy)defaultMetadataPolicy
|
|
{
|
|
return connectionDefaults.metadataPolicy;
|
|
}
|
|
|
|
- (void)setDefaultMetadataPolicy:(YapDatabasePolicy)defaultMetadataPolicy
|
|
{
|
|
connectionDefaults.metadataPolicy = defaultMetadataPolicy;
|
|
}
|
|
|
|
#if TARGET_OS_IOS || TARGET_OS_TV
|
|
|
|
- (YapDatabaseConnectionFlushMemoryFlags)defaultAutoFlushMemoryFlags
|
|
{
|
|
return connectionDefaults.autoFlushMemoryFlags;
|
|
}
|
|
|
|
- (void)setDefaultAutoFlushMemoryFlags:(YapDatabaseConnectionFlushMemoryFlags)defaultAutoFlushMemoryFlags
|
|
{
|
|
connectionDefaults.autoFlushMemoryFlags = defaultAutoFlushMemoryFlags;
|
|
}
|
|
|
|
#endif
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Connections
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* This method is called from [self newConnection].
|
|
**/
|
|
- (void)addConnection:(YapDatabaseConnection *)connection
|
|
{
|
|
// We can asynchronously add the connection to the state table.
|
|
// This is safe as the connection itself must go through the same queue in order to do anything.
|
|
//
|
|
// The primary motivation in adding the asynchronous functionality is due to the following common use case:
|
|
//
|
|
// YapDatabase *database = [[YapDatabase alloc] initWithPath:path];
|
|
// YapDatabaseConnection *databaseConnection = [database newConnection];
|
|
//
|
|
// The YapDatabase init method is asynchronously preparing itself through the snapshot queue.
|
|
// We'd like to avoid blocking the very next line of code and allow the asynchronous prepare to continue.
|
|
|
|
dispatch_async(connection->connectionQueue, ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
dispatch_sync(snapshotQueue, ^{ @autoreleasepool {
|
|
|
|
// Add the connection to the state table
|
|
|
|
YapDatabaseConnectionState *state = [[YapDatabaseConnectionState alloc] initWithConnection:connection];
|
|
[connectionStates addObject:state];
|
|
|
|
YDBLogVerbose(@"Created new connection(%p) for <%@ %p: databaseName=%@, connectionCount=%lu>",
|
|
connection, [self class], self, [databasePath lastPathComponent],
|
|
(unsigned long)[connectionStates count]);
|
|
|
|
// Invoke the one-time prepare method, so the connection can perform any needed initialization.
|
|
// Be sure to do this within the snapshotQueue, as the prepare method depends on this.
|
|
|
|
[connection prepare];
|
|
}});
|
|
|
|
#pragma clang diagnostic pop
|
|
});
|
|
}
|
|
|
|
/**
|
|
* This method is called from YapDatabaseConnection's dealloc method.
|
|
**/
|
|
- (void)removeConnection:(YapDatabaseConnection *)connection
|
|
{
|
|
dispatch_block_t block = ^{ @autoreleasepool {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
NSUInteger index = 0;
|
|
for (YapDatabaseConnectionState *state in connectionStates)
|
|
{
|
|
if (state->connection == connection)
|
|
{
|
|
[connectionStates removeObjectAtIndex:index];
|
|
break;
|
|
}
|
|
|
|
index++;
|
|
}
|
|
|
|
YDBLogVerbose(@"Removed connection(%p) from <%@ %p: databaseName=%@, connectionCount=%lu>",
|
|
connection, [self class], self, [databasePath lastPathComponent],
|
|
(unsigned long)[connectionStates count]);
|
|
|
|
#pragma clang diagnostic pop
|
|
}};
|
|
|
|
// We prefer to invoke this method synchronously.
|
|
//
|
|
// The connection may be the last object retaining the database.
|
|
// It's easier to trace object deallocations when they happen in a predictable order.
|
|
|
|
if (dispatch_get_specific(IsOnSnapshotQueueKey))
|
|
block();
|
|
else
|
|
dispatch_sync(snapshotQueue, block);
|
|
}
|
|
|
|
/**
|
|
* This is a public method called to create a new connection.
|
|
**/
|
|
- (YapDatabaseConnection *)newConnection
|
|
{
|
|
YapDatabaseConnection *connection = [[YapDatabaseConnection alloc] initWithDatabase:self];
|
|
|
|
[self addConnection:connection];
|
|
return connection;
|
|
}
|
|
|
|
/**
|
|
* This is a public method called to create a new connection.
|
|
**/
|
|
- (YapDatabaseConnection *)newConnection:(YapDatabaseConnectionConfig *)config
|
|
{
|
|
YapDatabaseConnection *connection = [[YapDatabaseConnection alloc] initWithDatabase:self config:config];
|
|
|
|
[self addConnection:connection];
|
|
return connection;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Extensions
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Registers the extension with the database using the given name.
|
|
* After registration everything works automatically using just the extension name.
|
|
*
|
|
* The registration process is equivalent to a (synchronous) readwrite transaction.
|
|
* It involves persisting various information about the extension to the database,
|
|
* as well as possibly populating the extension by enumerating existing rows in the database.
|
|
*
|
|
* @param extension
|
|
* The YapDatabaseExtension subclass instance you wish to register.
|
|
* For example, this might be a YapDatabaseView instance.
|
|
*
|
|
* @param extensionName
|
|
* This is an arbitrary string you assign to the extension.
|
|
* Once registered, you will generally access the extension instance via this name.
|
|
* For example: [[transaction ext:@"myView"] numberOfGroups];
|
|
*
|
|
* @return
|
|
* YES if the extension was properly registered.
|
|
* NO if an error occurred, such as the extensionName is already registered.
|
|
*
|
|
* @see asyncRegisterExtension:withName:completionBlock:
|
|
* @see asyncRegisterExtension:withName:completionQueue:completionBlock:
|
|
**/
|
|
- (BOOL)registerExtension:(YapDatabaseExtension *)extension withName:(NSString *)extensionName
|
|
{
|
|
return [self registerExtension:extension withName:extensionName config:nil];
|
|
}
|
|
|
|
/**
|
|
* Registers the extension with the database using the given name.
|
|
* After registration everything works automatically using just the extension name.
|
|
*
|
|
* The registration process is equivalent to a (synchronous) readwrite transaction.
|
|
* It involves persisting various information about the extension to the database,
|
|
* as well as possibly populating the extension by enumerating existing rows in the database.
|
|
*
|
|
* @param extension (required)
|
|
* The YapDatabaseExtension subclass instance you wish to register.
|
|
* For example, this might be a YapDatabaseView instance.
|
|
*
|
|
* @param extensionName (required)
|
|
* This is an arbitrary string you assign to the extension.
|
|
* Once registered, you will generally access the extension instance via this name.
|
|
* For example: [[transaction ext:@"myView"] numberOfGroups];
|
|
*
|
|
* @param config (optional)
|
|
* You may optionally pass a config for the internal databaseConnection used to perform
|
|
* the extension registration process. This allows you to control things such as the
|
|
* cache size, which is sometimes important for performance tuning.
|
|
*
|
|
* @see asyncRegisterExtension:withName:completionBlock:
|
|
* @see asyncRegisterExtension:withName:completionQueue:completionBlock:
|
|
**/
|
|
- (BOOL)registerExtension:(YapDatabaseExtension *)extension
|
|
withName:(NSString *)extensionName
|
|
config:(YapDatabaseConnectionConfig *)config
|
|
{
|
|
__block BOOL ready = NO;
|
|
dispatch_sync(writeQueue, ^{ @autoreleasepool {
|
|
|
|
ready = [self _registerExtension:extension withName:extensionName config:config];
|
|
}});
|
|
|
|
return ready;
|
|
}
|
|
|
|
/**
|
|
* Asynchronoulsy starts the extension registration process.
|
|
* After registration everything works automatically using just the extension name.
|
|
*
|
|
* The registration process is equivalent to an asyncReadwrite transaction.
|
|
* It involves persisting various information about the extension to the database,
|
|
* as well as possibly populating the extension by enumerating existing rows in the database.
|
|
*
|
|
* @param extension (required)
|
|
* The YapDatabaseExtension subclass instance you wish to register.
|
|
* For example, this might be a YapDatabaseView instance.
|
|
*
|
|
* @param extensionName (required)
|
|
* This is an arbitrary string you assign to the extension.
|
|
* Once registered, you will generally access the extension instance via this name.
|
|
* For example: [[transaction ext:@"myView"] numberOfGroups];
|
|
*
|
|
* @param completionBlock (optional)
|
|
* An optional completion block may be used.
|
|
* If the extension registration was successful then the ready parameter will be YES.
|
|
* The completionBlock will be invoked on the main thread (dispatch_get_main_queue()).
|
|
**/
|
|
- (void)asyncRegisterExtension:(YapDatabaseExtension *)extension
|
|
withName:(NSString *)extensionName
|
|
completionBlock:(void(^)(BOOL ready))completionBlock
|
|
{
|
|
[self asyncRegisterExtension:extension
|
|
withName:extensionName
|
|
config:nil
|
|
completionQueue:NULL
|
|
completionBlock:completionBlock];
|
|
}
|
|
|
|
/**
|
|
* Asynchronoulsy starts the extension registration process.
|
|
* After registration everything works automatically using just the extension name.
|
|
*
|
|
* The registration process is equivalent to an asyncReadwrite transaction.
|
|
* It involves persisting various information about the extension to the database,
|
|
* as well as possibly populating the extension by enumerating existing rows in the database.
|
|
*
|
|
* @param extension (required)
|
|
* The YapDatabaseExtension subclass instance you wish to register.
|
|
* For example, this might be a YapDatabaseView instance.
|
|
*
|
|
* @param extensionName (required)
|
|
* This is an arbitrary string you assign to the extension.
|
|
* Once registered, you will generally access the extension instance via this name.
|
|
* For example: [[transaction ext:@"myView"] numberOfGroups];
|
|
*
|
|
* @param completionQueue (optional)
|
|
* The dispatch_queue to invoke the completion block may optionally be specified.
|
|
* If NULL, dispatch_get_main_queue() is automatically used.
|
|
*
|
|
* @param completionBlock (optional)
|
|
* An optional completion block may be used.
|
|
* If the extension registration was successful then the ready parameter will be YES.
|
|
**/
|
|
- (void)asyncRegisterExtension:(YapDatabaseExtension *)extension
|
|
withName:(NSString *)extensionName
|
|
completionQueue:(dispatch_queue_t)completionQueue
|
|
completionBlock:(void(^)(BOOL ready))completionBlock
|
|
{
|
|
[self asyncRegisterExtension:extension
|
|
withName:extensionName
|
|
config:nil
|
|
completionQueue:completionQueue
|
|
completionBlock:completionBlock];
|
|
}
|
|
|
|
/**
|
|
* Asynchronoulsy starts the extension registration process.
|
|
* After registration everything works automatically using just the extension name.
|
|
*
|
|
* The registration process is equivalent to an asyncReadwrite transaction.
|
|
* It involves persisting various information about the extension to the database,
|
|
* as well as possibly populating the extension by enumerating existing rows in the database.
|
|
*
|
|
* @param extension (required)
|
|
* The YapDatabaseExtension subclass instance you wish to register.
|
|
* For example, this might be a YapDatabaseView instance.
|
|
*
|
|
* @param extensionName (required)
|
|
* This is an arbitrary string you assign to the extension.
|
|
* Once registered, you will generally access the extension instance via this name.
|
|
* For example: [[transaction ext:@"myView"] numberOfGroups];
|
|
*
|
|
* @param config (optional)
|
|
* You may optionally pass a config for the internal databaseConnection used to perform
|
|
* the extension registration process. This allows you to control things such as the
|
|
* cache size, which is sometimes important for performance tuning.
|
|
*
|
|
* @param completionBlock (optional)
|
|
* An optional completion block may be used.
|
|
* If the extension registration was successful then the ready parameter will be YES.
|
|
* The completionBlock will be invoked on the main thread (dispatch_get_main_queue()).
|
|
**/
|
|
- (void)asyncRegisterExtension:(YapDatabaseExtension *)extension
|
|
withName:(NSString *)extensionName
|
|
config:(YapDatabaseConnectionConfig *)config
|
|
completionBlock:(void(^)(BOOL ready))completionBlock
|
|
{
|
|
[self asyncRegisterExtension:extension
|
|
withName:extensionName
|
|
config:config
|
|
completionQueue:NULL
|
|
completionBlock:completionBlock];
|
|
}
|
|
|
|
/**
|
|
* Asynchronoulsy starts the extension registration process.
|
|
* After registration everything works automatically using just the extension name.
|
|
*
|
|
* The registration process is equivalent to an asyncReadwrite transaction.
|
|
* It involves persisting various information about the extension to the database,
|
|
* as well as possibly populating the extension by enumerating existing rows in the database.
|
|
*
|
|
* @param extension (required)
|
|
* The YapDatabaseExtension subclass instance you wish to register.
|
|
* For example, this might be a YapDatabaseView instance.
|
|
*
|
|
* @param extensionName (required)
|
|
* This is an arbitrary string you assign to the extension.
|
|
* Once registered, you will generally access the extension instance via this name.
|
|
* For example: [[transaction ext:@"myView"] numberOfGroups];
|
|
*
|
|
* @param config (optional)
|
|
* You may optionally pass a config for the internal databaseConnection used to perform
|
|
* the extension registration process. This allows you to control things such as the
|
|
* cache size, which is sometimes important for performance tuning.
|
|
*
|
|
* @param completionQueue (optional)
|
|
* The dispatch_queue to invoke the completion block may optionally be specified.
|
|
* If NULL, dispatch_get_main_queue() is automatically used.
|
|
*
|
|
* @param completionBlock (optional)
|
|
* An optional completion block may be used.
|
|
* If the extension registration was successful then the ready parameter will be YES.
|
|
**/
|
|
- (void)asyncRegisterExtension:(YapDatabaseExtension *)extension
|
|
withName:(NSString *)extensionName
|
|
config:(YapDatabaseConnectionConfig *)config
|
|
completionQueue:(dispatch_queue_t)completionQueue
|
|
completionBlock:(void(^)(BOOL ready))completionBlock
|
|
{
|
|
if (completionQueue == NULL && completionBlock != NULL)
|
|
completionQueue = dispatch_get_main_queue();
|
|
|
|
if (config)
|
|
config = [config copy];
|
|
|
|
dispatch_async(writeQueue, ^{ @autoreleasepool {
|
|
|
|
BOOL ready = [self _registerExtension:extension withName:extensionName config:config];
|
|
|
|
if (completionBlock)
|
|
{
|
|
dispatch_async(completionQueue, ^{ @autoreleasepool {
|
|
|
|
completionBlock(ready);
|
|
}});
|
|
}
|
|
}});
|
|
}
|
|
|
|
/**
|
|
* This method unregisters an extension with the given name.
|
|
* The associated underlying tables will be dropped from the database.
|
|
*
|
|
* The unregistration process is equivalent to a (synchronous) readwrite transaction.
|
|
* It involves deleting various information about the extension from the database,
|
|
* as well as possibly dropping related tables the extension may have been using.
|
|
*
|
|
* @param extensionName (required)
|
|
* This is the arbitrary string you assigned to the extension when you registered it.
|
|
*
|
|
* Note 1:
|
|
* You don't need to re-register an extension in order to unregister it. For example,
|
|
* you've previously registered an extension (in previous app launches), but you no longer need the extension.
|
|
* You don't have to bother creating and registering the unneeded extension,
|
|
* just so you can unregister it and have the associated tables dropped.
|
|
* The database persists information about registered extensions, including the associated class of an extension.
|
|
* So you can simply pass the name of the extension, and the database system will use the associated class to
|
|
* drop the appropriate tables.
|
|
*
|
|
* Note 2:
|
|
* In fact, you don't even have to worry about unregistering extensions that you no longer need.
|
|
* That database system will automatically handle it for you.
|
|
* That is, upon completion of the first readWrite transaction (that makes changes), the database system will
|
|
* check to see if there are any "orphaned" extensions. That is, previously registered extensions that are
|
|
* no longer in use (and are now out-of-date because they didn't process the recent change(s) to the db).
|
|
* And it will automatically unregister these orhpaned extensions for you.
|
|
*
|
|
* @see asyncUnregisterExtensionWithName:completionBlock:
|
|
* @see asyncUnregisterExtensionWithName:completionQueue:completionBlock:
|
|
**/
|
|
- (void)unregisterExtensionWithName:(NSString *)extensionName
|
|
{
|
|
dispatch_sync(writeQueue, ^{ @autoreleasepool {
|
|
|
|
[self _unregisterExtensionWithName:extensionName];
|
|
}});
|
|
}
|
|
|
|
/**
|
|
* Asynchronoulsy starts the extension unregistration process.
|
|
*
|
|
* The unregistration process is equivalent to an asyncReadwrite transaction.
|
|
* It involves deleting various information about the extension from the database,
|
|
* as well as possibly dropping related tables the extension may have been using.
|
|
*
|
|
* @param extensionName (required)
|
|
* This is the arbitrary string you assigned to the extension when you registered it.
|
|
*
|
|
* @param completionBlock (optional)
|
|
* An optional completion block may be used.
|
|
* The completionBlock will be invoked on the main thread (dispatch_get_main_queue()).
|
|
**/
|
|
- (void)asyncUnregisterExtensionWithName:(NSString *)extensionName
|
|
completionBlock:(dispatch_block_t)completionBlock
|
|
{
|
|
[self asyncUnregisterExtensionWithName:extensionName
|
|
completionQueue:NULL
|
|
completionBlock:completionBlock];
|
|
}
|
|
|
|
/**
|
|
* Asynchronoulsy starts the extension unregistration process.
|
|
*
|
|
* The unregistration process is equivalent to an asyncReadwrite transaction.
|
|
* It involves deleting various information about the extension from the database,
|
|
* as well as possibly dropping related tables the extension may have been using.
|
|
*
|
|
* @param extensionName (required)
|
|
* This is the arbitrary string you assigned to the extension when you registered it.
|
|
*
|
|
* @param completionQueue (optional)
|
|
* The dispatch_queue to invoke the completion block may optionally be specified.
|
|
* If NULL, dispatch_get_main_queue() is automatically used.
|
|
*
|
|
* @param completionBlock (optional)
|
|
* An optional completion block may be used.
|
|
**/
|
|
- (void)asyncUnregisterExtensionWithName:(NSString *)extensionName
|
|
completionQueue:(dispatch_queue_t)completionQueue
|
|
completionBlock:(dispatch_block_t)completionBlock
|
|
{
|
|
if (completionQueue == NULL && completionBlock != NULL)
|
|
completionQueue = dispatch_get_main_queue();
|
|
|
|
dispatch_async(writeQueue, ^{ @autoreleasepool {
|
|
|
|
[self _unregisterExtensionWithName:extensionName];
|
|
|
|
if (completionBlock)
|
|
{
|
|
dispatch_async(completionQueue, ^{ @autoreleasepool {
|
|
|
|
completionBlock();
|
|
}});
|
|
}
|
|
}});
|
|
}
|
|
|
|
/**
|
|
* Internal utility method.
|
|
* Handles lazy creation and destruction of short-lived registrationConnection instance.
|
|
*
|
|
* @see _registerExtension:withName:
|
|
* @see _unregisterExtensionWithName:
|
|
**/
|
|
- (YapDatabaseConnection *)registrationConnection
|
|
{
|
|
if (registrationConnection == nil)
|
|
{
|
|
registrationConnection = [self newConnection];
|
|
registrationConnection.name = @"YapDatabase_extensionRegistrationConnection";
|
|
|
|
// These are the rules (regarding instance retainCount):
|
|
// - a YapDatabaseConnection instance cannot be deallocated if there are existing/pending transactions
|
|
// - a YapDatabase instance cannot be deallocated if there are existing connections
|
|
//
|
|
// Thus, as long as registrationConnection is non-nil,
|
|
// 'self' (this YapDatabase instance) cannot be deallocated.
|
|
//
|
|
|
|
__weak YapDatabase *weakSelf = self;
|
|
|
|
NSTimeInterval delayInSeconds = 5.0;
|
|
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
|
|
dispatch_after(popTime, writeQueue, ^(void){
|
|
|
|
__strong YapDatabase *strongSelf = weakSelf;
|
|
if (strongSelf)
|
|
{
|
|
strongSelf->registrationConnection = nil;
|
|
}
|
|
});
|
|
}
|
|
|
|
return registrationConnection;
|
|
}
|
|
|
|
/**
|
|
* Internal method that handles extension registration.
|
|
* This method must be invoked on the writeQueue.
|
|
**/
|
|
- (BOOL)_registerExtension:(YapDatabaseExtension *)extension
|
|
withName:(NSString *)extensionName
|
|
config:(YapDatabaseConnectionConfig *)config
|
|
{
|
|
NSAssert(dispatch_get_specific(IsOnWriteQueueKey), @"Must go through writeQueue.");
|
|
|
|
// Validate parameters
|
|
|
|
if (extension == nil)
|
|
{
|
|
YDBLogError(@"Error registering extension: extension parameter is nil");
|
|
return NO;
|
|
}
|
|
if ([extensionName length] == 0)
|
|
{
|
|
YDBLogError(@"Error registering extension: extensionName parameter is nil or empty string");
|
|
return NO;
|
|
}
|
|
|
|
// Check to ensure extension isn't already registered,
|
|
// or that the extensionName isn't already taken.
|
|
|
|
NSDictionary *_registeredExtensions = [self registeredExtensions];
|
|
|
|
if (extension.registeredName != nil)
|
|
{
|
|
YDBLogError(@"Error registering extension: extension is already registered");
|
|
return NO;
|
|
}
|
|
if ([_registeredExtensions objectForKey:extensionName] != nil)
|
|
{
|
|
YDBLogError(@"Error registering extension: extensionName(%@) already registered", extensionName);
|
|
return NO;
|
|
}
|
|
|
|
// Attempt registration
|
|
|
|
extension.registeredName = extensionName;
|
|
extension.registeredDatabase = self;
|
|
|
|
BOOL result = [extension supportsDatabaseWithRegisteredExtensions:_registeredExtensions];
|
|
if (!result)
|
|
{
|
|
YDBLogError(@"Error registering extension: extension doesn't support database configuration");
|
|
}
|
|
else
|
|
{
|
|
YapDatabaseConnection *connection = [self registrationConnection];
|
|
|
|
YapDatabaseConnectionConfig *originalConfig = nil;
|
|
if (config)
|
|
{
|
|
originalConfig = [connection copyConfig];
|
|
[connection applyConfig:config];
|
|
}
|
|
|
|
result = [connection registerExtension:extension withName:extensionName];
|
|
|
|
if (config)
|
|
{
|
|
[connection applyConfig:originalConfig];
|
|
}
|
|
}
|
|
|
|
if (result)
|
|
{
|
|
[extension didRegisterExtension];
|
|
}
|
|
else
|
|
{
|
|
extension.registeredName = nil;
|
|
extension.registeredDatabase = nil;
|
|
}
|
|
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Internal method that handles extension unregistration.
|
|
* This method must be invoked on the writeQueue.
|
|
**/
|
|
- (void)_unregisterExtensionWithName:(NSString *)extensionName
|
|
{
|
|
NSAssert(dispatch_get_specific(IsOnWriteQueueKey), @"Must go through writeQueue.");
|
|
|
|
// Validate parameters
|
|
|
|
if ([extensionName length] == 0)
|
|
{
|
|
YDBLogError(@"Error unregistering extension: extensionName parameter is nil or empty string");
|
|
return;
|
|
}
|
|
|
|
// Perform unregistration
|
|
|
|
YapDatabaseConnection *connection = [self registrationConnection];
|
|
|
|
[connection unregisterExtensionWithName:extensionName];
|
|
}
|
|
|
|
/**
|
|
* Returns the registered extension with the given name.
|
|
**/
|
|
- (id)registeredExtension:(NSString *)extensionName
|
|
{
|
|
// This method is public
|
|
|
|
__block YapDatabaseExtension *result = nil;
|
|
|
|
dispatch_block_t block = ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
result = [registeredExtensions objectForKey:extensionName];
|
|
|
|
#pragma clang diagnostic pop
|
|
};
|
|
|
|
if (dispatch_get_specific(IsOnSnapshotQueueKey))
|
|
block();
|
|
else
|
|
dispatch_sync(snapshotQueue, block);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Returns all currently registered extensions as a dictionary.
|
|
* The key is the registed name (NSString), and the value is the extension (YapDatabaseExtension subclass).
|
|
**/
|
|
- (NSDictionary *)registeredExtensions
|
|
{
|
|
// This method is public
|
|
|
|
__block NSDictionary *extensionsCopy = nil;
|
|
|
|
dispatch_block_t block = ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
extensionsCopy = registeredExtensions;
|
|
|
|
#pragma clang diagnostic pop
|
|
};
|
|
|
|
if (dispatch_get_specific(IsOnSnapshotQueueKey))
|
|
block();
|
|
else
|
|
dispatch_sync(snapshotQueue, block);
|
|
|
|
return extensionsCopy;
|
|
}
|
|
|
|
/**
|
|
* This method is only accessible from within the snapshotQueue.
|
|
* Used by [YapDatabaseConnection prepare].
|
|
**/
|
|
- (NSArray *)extensionsOrder
|
|
{
|
|
NSAssert(dispatch_get_specific(IsOnSnapshotQueueKey), @"Must go through snapshotQueue for atomic access.");
|
|
|
|
return extensionsOrder;
|
|
}
|
|
|
|
/**
|
|
* This method is only accessible from within the snapshotQueue.
|
|
* Used by [YapDatabaseConnection prepare].
|
|
**/
|
|
- (NSDictionary *)extensionDependencies
|
|
{
|
|
NSAssert(dispatch_get_specific(IsOnSnapshotQueueKey), @"Must go through snapshotQueue for atomic access.");
|
|
|
|
return extensionDependencies;
|
|
}
|
|
|
|
/**
|
|
* Allows you to fetch the registered extension names from the last time the database was run.
|
|
* Typically this means from the last time the app was run.
|
|
*
|
|
* This may be used to assist in various tasks, such as cleanup or upgrade tasks.
|
|
*
|
|
* If you need this information, you should fetch it early on because YapDatabase only maintains this information
|
|
* until it sees you are done registering all your initial extensions. That is, after one initializes the database
|
|
* they then immediately register any needed initial extensions before they begin to use the database. Once a
|
|
* readWriteTransaction modifies the database, YapDatabase will take this opportunity to look for orphaned extensions.
|
|
* These are extensions that were registered at the end of the last database session,
|
|
* but which are no longer registered. YapDatabase will automatically cleanup these orphaned extensions,
|
|
* and also clear the previouslyRegisteredExtensionNames information at this point.
|
|
**/
|
|
- (NSArray *)previouslyRegisteredExtensionNames
|
|
{
|
|
__block NSArray *result = nil;
|
|
|
|
dispatch_block_t block = ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
result = [previouslyRegisteredExtensionNames copy];
|
|
|
|
#pragma clang diagnostic pop
|
|
};
|
|
|
|
if (dispatch_get_specific(IsOnSnapshotQueueKey))
|
|
block();
|
|
else
|
|
dispatch_sync(snapshotQueue, block);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* It's sometimes useful to find out when all async registerExtension/unregisterExtension requests have completed.
|
|
*
|
|
* One way to accomplish this is simply to queue an asyncReadWriteTransaction on any databaseConnection.
|
|
* Since all async register/unregister extension requests are immediately dispatch_async'd through the
|
|
* internal serial writeQueue, you'll know that once your asyncReadWriteTransaction is running,
|
|
* all previously scheduled register/unregister requests have completed.
|
|
*
|
|
* Although the above technique works, the 'flushExtensionRequestsWithCompletionQueue::'
|
|
* is a more efficient way to accomplish this task. (And a more elegant & readable way too.)
|
|
*
|
|
* @param completionQueue
|
|
* The dispatch_queue to invoke the completionBlock on.
|
|
* If NULL, dispatch_get_main_queue() is automatically used.
|
|
*
|
|
* @param completionBlock
|
|
* The block to invoke once all previously scheduled register/unregister extension requests have completed.
|
|
**/
|
|
- (void)flushExtensionRequestsWithCompletionQueue:(nullable dispatch_queue_t)completionQueue
|
|
completionBlock:(nullable dispatch_block_t)completionBlock
|
|
{
|
|
if (completionQueue == NULL && completionBlock != NULL)
|
|
completionQueue = dispatch_get_main_queue();
|
|
|
|
dispatch_async(writeQueue, ^{ @autoreleasepool {
|
|
|
|
if (completionBlock)
|
|
{
|
|
dispatch_async(completionQueue, ^{ @autoreleasepool {
|
|
|
|
completionBlock();
|
|
}});
|
|
}
|
|
}});
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Pooling
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
- (NSUInteger)maxConnectionPoolCount
|
|
{
|
|
__block NSUInteger count = 0;
|
|
|
|
dispatch_sync(internalQueue, ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
count = maxConnectionPoolCount;
|
|
|
|
#pragma clang diagnostic pop
|
|
});
|
|
|
|
return count;
|
|
}
|
|
|
|
- (void)setMaxConnectionPoolCount:(NSUInteger)count
|
|
{
|
|
dispatch_sync(internalQueue, ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
// Update ivar
|
|
maxConnectionPoolCount = count;
|
|
|
|
// Immediately drop any excess connections
|
|
if ([connectionPoolValues count] > maxConnectionPoolCount)
|
|
{
|
|
do
|
|
{
|
|
sqlite3 *aDb = (sqlite3 *)[[connectionPoolValues objectAtIndex:0] pointerValue];
|
|
|
|
int status = sqlite3_close(aDb);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Error in sqlite_close: %d %s", status, sqlite3_errmsg(aDb));
|
|
}
|
|
|
|
[connectionPoolValues removeObjectAtIndex:0];
|
|
[connectionPoolDates removeObjectAtIndex:0];
|
|
|
|
} while ([connectionPoolValues count] > maxConnectionPoolCount);
|
|
|
|
[self resetConnectionPoolTimer];
|
|
}
|
|
|
|
#pragma clang diagnostic pop
|
|
});
|
|
}
|
|
|
|
- (NSTimeInterval)connectionPoolLifetime
|
|
{
|
|
__block NSTimeInterval lifetime = 0;
|
|
|
|
dispatch_sync(internalQueue, ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
lifetime = connectionPoolLifetime;
|
|
|
|
#pragma clang diagnostic pop
|
|
});
|
|
|
|
return lifetime;
|
|
}
|
|
|
|
- (void)setConnectionPoolLifetime:(NSTimeInterval)lifetime
|
|
{
|
|
dispatch_sync(internalQueue, ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
// Update ivar
|
|
connectionPoolLifetime = lifetime;
|
|
|
|
// Update timer (if needed)
|
|
[self resetConnectionPoolTimer];
|
|
|
|
#pragma clang diagnostic pop
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adds the given connection to the connection pool if possible.
|
|
*
|
|
* Returns YES if the instance was added to the pool.
|
|
* If so, the YapDatabaseConnection must not close the instance.
|
|
*
|
|
* Returns NO if the instance was not added to the pool.
|
|
* If so, the YapDatabaseConnection must close the instance.
|
|
**/
|
|
- (BOOL)connectionPoolEnqueue:(sqlite3 *)aDb main_file:(yap_file *)main_file wal_file:(yap_file *)wal_file
|
|
{
|
|
__block BOOL result = NO;
|
|
|
|
dispatch_sync(internalQueue, ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
if ([connectionPoolValues count] < maxConnectionPoolCount)
|
|
{
|
|
if (connectionPoolValues == nil)
|
|
{
|
|
connectionPoolValues = [[NSMutableArray alloc] init];
|
|
connectionPoolDates = [[NSMutableArray alloc] init];
|
|
}
|
|
|
|
YDBLogVerbose(@"Enqueuing connection to pool: %p", aDb);
|
|
|
|
NSDictionary *value = @{
|
|
YDBConnectionPoolValueKey_db : [NSValue valueWithPointer:(const void *)aDb],
|
|
YDBConnectionPoolValueKey_main_file : [NSValue valueWithPointer:(const void *)main_file],
|
|
YDBConnectionPoolValueKey_wal_file : [NSValue valueWithPointer:(const void *)wal_file],
|
|
};
|
|
|
|
[connectionPoolValues addObject:value];
|
|
[connectionPoolDates addObject:[NSDate date]];
|
|
|
|
result = YES;
|
|
|
|
if ([connectionPoolValues count] == 1)
|
|
{
|
|
[self resetConnectionPoolTimer];
|
|
}
|
|
}
|
|
|
|
#pragma clang diagnostic pop
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Retrieves a connection from the connection pool if available.
|
|
* Returns NULL if no connections are available.
|
|
**/
|
|
- (BOOL)connectionPoolDequeue:(sqlite3 **)pDb main_file:(yap_file **)pMainFile wal_file:(yap_file **)pWalFile
|
|
{
|
|
NSParameterAssert(pDb != NULL);
|
|
NSParameterAssert(pMainFile != NULL);
|
|
NSParameterAssert(pWalFile != NULL);
|
|
|
|
__block sqlite3 *aDb = NULL;
|
|
__block yap_file *main_file = NULL;
|
|
__block yap_file *wal_file = NULL;
|
|
|
|
dispatch_sync(internalQueue, ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
if ([connectionPoolValues count] > 0)
|
|
{
|
|
NSDictionary *value = [connectionPoolValues objectAtIndex:0];
|
|
|
|
aDb = (sqlite3 *)[[value objectForKey:YDBConnectionPoolValueKey_db] pointerValue];
|
|
main_file = (yap_file *)[[value objectForKey:YDBConnectionPoolValueKey_main_file] pointerValue];
|
|
wal_file = (yap_file *)[[value objectForKey:YDBConnectionPoolValueKey_wal_file] pointerValue];
|
|
|
|
YDBLogVerbose(@"Dequeuing connection from pool: %p", aDb);
|
|
|
|
[connectionPoolValues removeObjectAtIndex:0];
|
|
[connectionPoolDates removeObjectAtIndex:0];
|
|
|
|
[self resetConnectionPoolTimer];
|
|
}
|
|
|
|
#pragma clang diagnostic pop
|
|
});
|
|
|
|
*pDb = aDb;
|
|
*pMainFile = main_file;
|
|
*pWalFile = wal_file;
|
|
|
|
return (aDb != NULL);
|
|
}
|
|
|
|
/**
|
|
* Internal utility method to handle setting/resetting the timer.
|
|
**/
|
|
- (void)resetConnectionPoolTimer
|
|
{
|
|
YDBLogAutoTrace();
|
|
|
|
if (connectionPoolLifetime <= 0.0 || [connectionPoolValues count] == 0)
|
|
{
|
|
if (connectionPoolTimer)
|
|
{
|
|
dispatch_source_cancel(connectionPoolTimer);
|
|
connectionPoolTimer = NULL;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
BOOL isNewTimer = NO;
|
|
|
|
if (connectionPoolTimer == NULL)
|
|
{
|
|
connectionPoolTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, internalQueue);
|
|
|
|
__weak YapDatabase *weakSelf = self;
|
|
dispatch_source_set_event_handler(connectionPoolTimer, ^{ @autoreleasepool {
|
|
|
|
__strong YapDatabase *strongSelf = weakSelf;
|
|
if (strongSelf)
|
|
{
|
|
[strongSelf handleConnectionPoolTimerFire];
|
|
}
|
|
}});
|
|
|
|
#if !OS_OBJECT_USE_OBJC
|
|
dispatch_source_t timer = connectionPoolTimer;
|
|
dispatch_source_set_cancel_handler(connectionPoolTimer, ^{
|
|
dispatch_release(timer);
|
|
});
|
|
#endif
|
|
|
|
isNewTimer = YES;
|
|
}
|
|
|
|
NSDate *date = [connectionPoolDates objectAtIndex:0];
|
|
NSTimeInterval interval = [date timeIntervalSinceNow] + connectionPoolLifetime;
|
|
|
|
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(interval * NSEC_PER_SEC));
|
|
dispatch_source_set_timer(connectionPoolTimer, tt, DISPATCH_TIME_FOREVER, 0);
|
|
|
|
if (isNewTimer) {
|
|
dispatch_resume(connectionPoolTimer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal method to handle removing stale connections from the connection pool.
|
|
**/
|
|
- (void)handleConnectionPoolTimerFire
|
|
{
|
|
YDBLogAutoTrace();
|
|
|
|
NSDate *now = [NSDate date];
|
|
|
|
BOOL done = NO;
|
|
while ([connectionPoolValues count] > 0 && !done)
|
|
{
|
|
NSTimeInterval interval = [[connectionPoolDates objectAtIndex:0] timeIntervalSinceDate:now] * -1.0;
|
|
|
|
if ((interval >= connectionPoolLifetime) || (interval < 0))
|
|
{
|
|
NSDictionary *value = [connectionPoolValues objectAtIndex:0];
|
|
|
|
sqlite3 *aDb = (sqlite3 *)[[value objectForKey:YDBConnectionPoolValueKey_db] pointerValue];
|
|
|
|
YDBLogVerbose(@"Closing connection from pool: %p", aDb);
|
|
|
|
int status = sqlite3_close(aDb);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"Error in sqlite_close: %d %s", status, sqlite3_errmsg(aDb));
|
|
}
|
|
|
|
[connectionPoolValues removeObjectAtIndex:0];
|
|
[connectionPoolDates removeObjectAtIndex:0];
|
|
}
|
|
else
|
|
{
|
|
done = YES;
|
|
}
|
|
}
|
|
|
|
[self resetConnectionPoolTimer];
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Memory Tables
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* This method is only accessible from within the snapshotQueue.
|
|
* Used by [YapDatabaseConnection prepare].
|
|
**/
|
|
- (NSDictionary *)registeredMemoryTables
|
|
{
|
|
NSAssert(dispatch_get_specific(IsOnSnapshotQueueKey), @"Must go through snapshotQueue for atomic access.");
|
|
|
|
return registeredMemoryTables;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Snapshot Architecture
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* The snapshot represents when the database was last modified by a read-write transaction.
|
|
* This information isn persisted to the 'yap' database, and is separately held in memory.
|
|
* It serves multiple purposes.
|
|
*
|
|
* First is assists in validation of a connection's cache.
|
|
* When a connection begins a new transaction, it may have items sitting in the cache.
|
|
* However the connection doesn't know if the items are still valid because another connection may have made changes.
|
|
*
|
|
* The snapshot also assists in correcting for a race condition.
|
|
* It order to minimize blocking we allow read-write transactions to commit outside the context
|
|
* of the snapshotQueue. This is because the commit may be a time consuming operation, and we
|
|
* don't want to block read-only transactions during this period. The race condition occurs if a read-only
|
|
* transactions starts in the midst of a read-write commit, and the read-only transaction gets
|
|
* a "yap-level" snapshot that's out of sync with the "sql-level" snapshot. This is easily correctable if caught.
|
|
* Thus we maintain the snapshot in memory, and fetchable via a select query.
|
|
* One represents the "yap-level" snapshot, and the other represents the "sql-level" snapshot.
|
|
*
|
|
* The snapshot is simply a 64-bit integer.
|
|
* It is reset when the YapDatabase instance is initialized,
|
|
* and incremented by each read-write transaction (if changes are actually made).
|
|
**/
|
|
- (uint64_t)snapshot
|
|
{
|
|
if (dispatch_get_specific(IsOnSnapshotQueueKey))
|
|
{
|
|
// Very common case.
|
|
// This method is called on just about every transaction.
|
|
return snapshot;
|
|
}
|
|
else
|
|
{
|
|
// Non-common case.
|
|
// Public access implementation.
|
|
__block uint64_t result = 0;
|
|
|
|
dispatch_sync(snapshotQueue, ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
result = snapshot;
|
|
|
|
#pragma clang diagnostic pop
|
|
});
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method is only accessible from within the snapshotQueue.
|
|
*
|
|
* Prior to starting the sqlite commit, the connection must report its changeset to the database.
|
|
* The database will store the changeset, and provide it to other connections if needed (due to a race condition).
|
|
*
|
|
* The following MUST be in the dictionary:
|
|
*
|
|
* - snapshot : NSNumber with the changeset's snapshot
|
|
**/
|
|
- (void)notePendingChangeset:(NSDictionary *)pendingChangeset fromConnection:(YapDatabaseConnection __unused *)sender
|
|
{
|
|
NSAssert(dispatch_get_specific(IsOnSnapshotQueueKey), @"Must go through snapshotQueue for atomic access.");
|
|
NSAssert([pendingChangeset objectForKey:YapDatabaseSnapshotKey], @"Missing required change key: snapshot");
|
|
|
|
// The sender is preparing to start the sqlite commit.
|
|
// We save the changeset in advance to handle possible edge cases.
|
|
|
|
[changesets addObject:pendingChangeset];
|
|
|
|
YDBLogVerbose(@"Adding pending changeset %@ for database: %@",
|
|
[[changesets lastObject] objectForKey:YapDatabaseSnapshotKey], self);
|
|
}
|
|
|
|
/**
|
|
* This method is only accessible from within the snapshotQueue.
|
|
*
|
|
* This method is used if a transaction finds itself in a race condition.
|
|
* That is, the transaction started before it was able to process changesets from sibling connections.
|
|
*
|
|
* It should fetch the changesets needed and then process them via [connection noteCommittedChangeset:].
|
|
*
|
|
* Returns `nil` if the number of changesets found is not the expected one, that is, one for each snapshot increase from `connectionSnapshot` to `maxSnapshot`.
|
|
* This can only happen in multiprocess mode, if another process has updated the database.
|
|
* In this case the changesets are invalid, and we need to clear connection and extension caches.
|
|
**/
|
|
- (NSArray *)pendingAndCommittedChangesetsSince:(uint64_t)connectionSnapshot until:(uint64_t)maxSnapshot
|
|
{
|
|
NSAssert(dispatch_get_specific(IsOnSnapshotQueueKey), @"Must go through snapshotQueue for atomic access.");
|
|
|
|
NSUInteger capacity = (NSUInteger)(maxSnapshot - connectionSnapshot);
|
|
NSMutableArray *relevantChangesets = [NSMutableArray arrayWithCapacity:capacity];
|
|
|
|
for (NSDictionary *changeset in changesets)
|
|
{
|
|
uint64_t changesetSnapshot = [[changeset objectForKey:YapDatabaseSnapshotKey] unsignedLongLongValue];
|
|
|
|
if ((changesetSnapshot > connectionSnapshot) && (changesetSnapshot <= maxSnapshot))
|
|
{
|
|
[relevantChangesets addObject:changeset];
|
|
}
|
|
}
|
|
|
|
if (options.enableMultiProcessSupport)
|
|
{
|
|
const uint64_t expectedSnapshotsCount = maxSnapshot - connectionSnapshot;
|
|
if (expectedSnapshotsCount != relevantChangesets.count)
|
|
{
|
|
YDBLogVerbose(@"Expected snapshot count not found: expected(%llu) != found(%llu)."
|
|
@" Database seems to have been modified from another process. Discarding changeset.",
|
|
expectedSnapshotsCount, (uint64_t)relevantChangesets.count);
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
return relevantChangesets;
|
|
}
|
|
|
|
/**
|
|
* This method is only accessible from within the snapshotQueue.
|
|
*
|
|
* Upon completion of a readwrite transaction, the connection should report it's changeset to the database.
|
|
* The database will then forward the changes to all other connection's.
|
|
*
|
|
* The following MUST be in the dictionary:
|
|
*
|
|
* - snapshot : NSNumber with the changeset's snapshot
|
|
**/
|
|
- (void)noteCommittedChangeset:(NSDictionary *)changeset fromConnection:(YapDatabaseConnection *)sender
|
|
{
|
|
NSAssert(dispatch_get_specific(IsOnSnapshotQueueKey), @"Must go through snapshotQueue for atomic access.");
|
|
NSAssert([changeset objectForKey:YapDatabaseSnapshotKey], @"Missing required change key: snapshot");
|
|
|
|
// The sender has finished the sqlite commit, and all data is now written to disk.
|
|
|
|
// Update the in-memory snapshot,
|
|
// which represents the most recent snapshot of the last committed readwrite transaction.
|
|
|
|
snapshot = [[changeset objectForKey:YapDatabaseSnapshotKey] unsignedLongLongValue];
|
|
|
|
// Update registeredExtensions, if changed.
|
|
|
|
NSDictionary *newRegisteredExtensions = [changeset objectForKey:YapDatabaseRegisteredExtensionsKey];
|
|
if (newRegisteredExtensions)
|
|
{
|
|
registeredExtensions = newRegisteredExtensions;
|
|
extensionsOrder = [changeset objectForKey:YapDatabaseExtensionsOrderKey];
|
|
extensionDependencies = [changeset objectForKey:YapDatabaseExtensionDependenciesKey];
|
|
}
|
|
|
|
// Update registeredMemoryTables, if changed.
|
|
|
|
NSDictionary *newRegisteredMemoryTables = [changeset objectForKey:YapDatabaseRegisteredMemoryTablesKey];
|
|
if (newRegisteredMemoryTables)
|
|
{
|
|
registeredMemoryTables = newRegisteredMemoryTables;
|
|
}
|
|
|
|
// Forward the changeset to all extensions.
|
|
|
|
[registeredExtensions enumerateKeysAndObjectsUsingBlock:
|
|
^(NSString *extName, YapDatabaseExtension *ext, BOOL __unused *stop)
|
|
{
|
|
[ext noteCommittedChangeset:changeset registeredName:extName];
|
|
}];
|
|
|
|
// Forward the changeset to all other connections so they can perform any needed updates.
|
|
// Generally this means updating the in-memory components such as the cache.
|
|
|
|
NSMutableArray<YapDatabaseConnection *> *strongConnections = nil;
|
|
dispatch_group_t group = NULL;
|
|
|
|
for (YapDatabaseConnectionState *state in connectionStates)
|
|
{
|
|
if (state->connection != sender)
|
|
{
|
|
// Create strong reference (state->connection is weak)
|
|
__strong YapDatabaseConnection *connection = state->connection;
|
|
|
|
if (connection)
|
|
{
|
|
if (strongConnections == nil)
|
|
strongConnections = [NSMutableArray array];
|
|
|
|
[strongConnections addObject:connection];
|
|
|
|
if (group == NULL)
|
|
group = dispatch_group_create();
|
|
|
|
dispatch_group_async(group, connection->connectionQueue, ^{ @autoreleasepool {
|
|
|
|
[connection noteCommittedChangeset:changeset];
|
|
}});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Schedule block to be executed once all connections have processed the changes.
|
|
|
|
BOOL isInternalChangeset = (sender == nil);
|
|
__weak YapDatabase *weakSelf = self;
|
|
|
|
dispatch_block_t block = ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic warning "-Wimplicit-retain-self" // Turning warnings *** ON ***
|
|
|
|
__strong YapDatabase *strongSelf = weakSelf;
|
|
if (strongSelf == nil) return;
|
|
|
|
// All connections have now processed the changes.
|
|
// So we no longer need to retain the changeset in memory.
|
|
|
|
if (isInternalChangeset)
|
|
{
|
|
YDBLogVerbose(@"Completed internal changeset %@ for database: %@",
|
|
[changeset objectForKey:YapDatabaseSnapshotKey], self);
|
|
}
|
|
else
|
|
{
|
|
YDBLogVerbose(@"Dropping processed changeset %@ for database: %@",
|
|
[changeset objectForKey:YapDatabaseSnapshotKey], self);
|
|
|
|
[strongSelf->changesets removeObjectAtIndex:0];
|
|
}
|
|
|
|
#if !OS_OBJECT_USE_OBJC
|
|
if (group)
|
|
dispatch_release(group);
|
|
#endif
|
|
|
|
#pragma clang diagnostic pop
|
|
};
|
|
|
|
if (group)
|
|
dispatch_group_notify(group, snapshotQueue, block);
|
|
else
|
|
block();
|
|
|
|
if (strongConnections)
|
|
{
|
|
// Edge case protection:
|
|
// Bug fix for issues: #437, #441
|
|
//
|
|
// Deadlock crash if:
|
|
// - YapDatabase is the last one holding a strong reference to a YapDatabaseConnection instance
|
|
// - The [connection dealloc] call occurs within the snapshotQueue
|
|
//
|
|
// This is a workaround to ensure that the dealloc occurs outside the snapshotQueue.
|
|
//
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @autoreleasepool {
|
|
[strongConnections removeAllObjects];
|
|
}});
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Manual Checkpointing
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* This method should be called whenever the maximum checkpointable snapshot is incremented.
|
|
* That is, the state of every connection is known to the system.
|
|
* And a snaphot cannot be checkpointed until every connection is at or past that snapshot.
|
|
* Thus, we can know the point at which a snapshot becomes checkpointable,
|
|
* and we can thus optimize the checkpoint invocations such that
|
|
* each invocation is able to checkpoint one or more commits.
|
|
**/
|
|
- (void)asyncCheckpoint:(uint64_t)maxCheckpointableSnapshot
|
|
{
|
|
if (maxCheckpointableSnapshot > 0) {
|
|
YDBLogVerbose(@"Checkpoint possible up to snapshot %llu", maxCheckpointableSnapshot);
|
|
}
|
|
|
|
bool aggressive = atomic_load(&aggressiveCheckpointEnabled);
|
|
if (aggressive)
|
|
{
|
|
[self asyncAggressiveCheckpoint];
|
|
}
|
|
else
|
|
{
|
|
[self asyncPassiveCheckpoint];
|
|
}
|
|
}
|
|
|
|
- (void)asyncPassiveCheckpoint
|
|
{
|
|
bool hasPendingCheckpoint = atomic_flag_test_and_set(&pendingPassiveCheckpoint);
|
|
if (hasPendingCheckpoint) {
|
|
return;
|
|
}
|
|
|
|
__weak YapDatabase *weakSelf = self;
|
|
|
|
dispatch_async(checkpointQueue, ^{ @autoreleasepool {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic warning "-Wimplicit-retain-self" // Turning warnings *** ON ***
|
|
|
|
__strong YapDatabase *strongSelf = weakSelf;
|
|
if (strongSelf == nil) return;
|
|
|
|
atomic_flag_clear(&strongSelf->pendingPassiveCheckpoint);
|
|
|
|
if (atomic_load(&strongSelf->aggressiveCheckpointEnabled)) {
|
|
return;
|
|
}
|
|
|
|
[strongSelf passiveCheckpoint];
|
|
|
|
#pragma clang diagnostic pop
|
|
}});
|
|
}
|
|
|
|
- (void)asyncAggressiveCheckpoint
|
|
{
|
|
bool hasPendingCheckpoint = atomic_flag_test_and_set(&pendingAggressiveCheckpoint);
|
|
if (hasPendingCheckpoint) {
|
|
return;
|
|
}
|
|
|
|
__weak YapDatabase *weakSelf = self;
|
|
|
|
dispatch_async(writeQueue, ^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic warning "-Wimplicit-retain-self" // Turning warnings *** ON ***
|
|
|
|
__strong YapDatabase *strongSelf = weakSelf;
|
|
if (strongSelf == nil) return;
|
|
|
|
atomic_flag_clear(&strongSelf->pendingAggressiveCheckpoint);
|
|
|
|
if (!atomic_load(&strongSelf->aggressiveCheckpointEnabled)) {
|
|
return;
|
|
}
|
|
|
|
[strongSelf aggressiveCheckpoint];
|
|
|
|
#pragma clang diagnostic pop
|
|
});
|
|
}
|
|
|
|
- (void)passiveCheckpoint
|
|
{
|
|
int checkpointResult = 0;
|
|
int totalFrameCount = 0;
|
|
int checkpointedFrameCount = 0;
|
|
|
|
// We're going to execute a passive checkpoint.
|
|
// That is, without disrupting any connections, we're going to write pages from the WAL into the database.
|
|
// The checkpoint can only write pages from snapshots if all connections are at or beyond the snapshot.
|
|
// Thus, this method is only called by a connection that moves the min snapshot forward.
|
|
|
|
checkpointResult = sqlite3_wal_checkpoint_v2(db, "main", SQLITE_CHECKPOINT_PASSIVE,
|
|
&totalFrameCount, &checkpointedFrameCount);
|
|
|
|
// totalFrameCount = total number of frames in the WAL file
|
|
// checkpointedFrameCount = total number of checkpointed frames (those copied into db file)
|
|
// (including any that were already checkpointed before the function was called)
|
|
|
|
YDBLogVerbose(@"Post-checkpoint: src(a) mode(passive) result(%d) frames(%d) checkpointed(%d)",
|
|
checkpointResult, totalFrameCount, checkpointedFrameCount);
|
|
|
|
if (checkpointResult != SQLITE_OK)
|
|
{
|
|
if (checkpointResult == SQLITE_BUSY) {
|
|
YDBLogVerbose(@"sqlite3_wal_checkpoint_v2 returned SQLITE_BUSY");
|
|
}
|
|
else {
|
|
YDBLogWarn(@"sqlite3_wal_checkpoint_v2 returned error code: %d", checkpointResult);
|
|
}
|
|
|
|
return;// from_block
|
|
}
|
|
|
|
// Did we checkpoint the entire WAL file ?
|
|
|
|
BOOL didCheckpointEntireWAL = (totalFrameCount == checkpointedFrameCount);
|
|
|
|
if (didCheckpointEntireWAL)
|
|
{
|
|
// We've checkpointed every single frame in the WAL.
|
|
// This means the next read-write transaction may be able to reset the WAL (instead of appending to it).
|
|
//
|
|
// However, the WAL reset will get spoiled if there are active read-only transactions that
|
|
// were started before our checkpoint finished, and continue to exist during the next read-write.
|
|
// It's not a big deal if the occasional read-only transaction happens to spoil the WAL reset.
|
|
// In those cases, the WAL generally gets reset shortly thereafter (on a subsequent write).
|
|
// Long-lived read transactions are a different case entirely.
|
|
// These transactions spoil it every single time, and could potentially cause the WAL to grow indefinitely.
|
|
//
|
|
// The solution is to notify active long-lived connections, and tell them to re-begin their transaction
|
|
// on the same snapshot. But this time the sqlite machinery will read directly from the database,
|
|
// and thus unlock the WAL so it can be reset.
|
|
|
|
__weak YapDatabase *weakSelf = self;
|
|
|
|
dispatch_async(writeQueue, ^{ @autoreleasepool {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic warning "-Wimplicit-retain-self" // Turning warnings *** ON ***
|
|
|
|
__strong YapDatabase *strongSelf = weakSelf;
|
|
if (strongSelf == nil) return;
|
|
|
|
[strongSelf tryResetLongLivedReadTransactions];
|
|
|
|
#pragma clang diagnostic pop
|
|
}});
|
|
}
|
|
|
|
// Is the WAL file getting too big ?
|
|
|
|
uint64_t walApproximateFileSize = totalFrameCount * pageSize;
|
|
BOOL needsAggressiveCheckpoint = (walApproximateFileSize >= options.aggressiveWALTruncationSize);
|
|
|
|
if (needsAggressiveCheckpoint)
|
|
{
|
|
atomic_store(&aggressiveCheckpointEnabled, true);
|
|
|
|
[self asyncAggressiveCheckpoint];
|
|
}
|
|
}
|
|
|
|
- (void)aggressiveCheckpoint
|
|
{
|
|
int checkpointResult = 0;
|
|
int totalFrameCount = 0;
|
|
int checkpointedFrameCount = 0;
|
|
|
|
// First we set an adequate busy timeout on our database connection.
|
|
// We're going to run a non-passive checkpoint.
|
|
// Which may cause it to busy-wait while waiting on read transactions to complete.
|
|
|
|
sqlite3_busy_timeout(db, 50); // milliseconds
|
|
|
|
// Step 1 of 3:
|
|
//
|
|
// Perform FULL checkpoint.
|
|
//
|
|
// This will checkpoint as many frames as possible,
|
|
// and busy-wait until all readers are on the latest commit.
|
|
|
|
checkpointResult = sqlite3_wal_checkpoint_v2(db, "main", SQLITE_CHECKPOINT_FULL,
|
|
&totalFrameCount, &checkpointedFrameCount);
|
|
|
|
YDBLogInfo(@"Post-checkpoint: src(b) mode(full) result(%d) frames(%d) checkpointed(%d)",
|
|
checkpointResult, totalFrameCount, checkpointedFrameCount);
|
|
|
|
if (totalFrameCount != checkpointedFrameCount)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// STEP 2 of 3:
|
|
//
|
|
// Check for longLivedReadTransactions, and attempt to silently move them to reading directly from the database.
|
|
// (As oppossed to reading from the latest commit in the WAL.)
|
|
|
|
if (![self tryResetLongLivedReadTransactions])
|
|
{
|
|
YDBLogInfo(@"Aggressive checkpoint spoiled: longLivedReadTransaction is blocking");
|
|
return;
|
|
}
|
|
|
|
// STEP 3 of 3:
|
|
//
|
|
// Perform TRUNCATE checkpoint.
|
|
//
|
|
// At this point, we've checkpointed every single frame.
|
|
// And every connection should be reading directly from the database.
|
|
// So we should be able to truncate the WAL file now.
|
|
|
|
// Can we use SQLITE_CHECKPOINT_TRUNCATE ?
|
|
//
|
|
// This feature was added in sqlite v3.8.8.
|
|
// But it was buggy until v3.8.8.2 when the following fix was added:
|
|
//
|
|
// "Enhance sqlite3_wal_checkpoint_v2(TRUNCATE) interface so that it truncates the
|
|
// WAL file even if there is no checkpoint work to be done."
|
|
//
|
|
// http://www.sqlite.org/changes.html
|
|
//
|
|
// It is often the case, when we call checkpoint here, that there is no checkpoint work to be done.
|
|
// So we really can't depend on it until 3.8.8.2
|
|
|
|
int checkpointMode = SQLITE_CHECKPOINT_RESTART;
|
|
|
|
// Remember: The compiler defines (SQLITE_VERSION, SQLITE_VERSION_NUMBER) only tell us
|
|
// what version we're compiling against. But we may encounter an earlier sqlite version at runtime.
|
|
|
|
#ifndef SQLITE_VERSION_NUMBER_3_8_8
|
|
#define SQLITE_VERSION_NUMBER_3_8_8 3008008
|
|
#endif
|
|
|
|
#if SQLITE_VERSION_NUMBER > SQLITE_VERSION_NUMBER_3_8_8
|
|
|
|
checkpointMode = SQLITE_CHECKPOINT_TRUNCATE;
|
|
|
|
#elif SQLITE_VERSION_NUMBER == SQLITE_VERSION_NUMBER_3_8_8
|
|
|
|
NSComparisonResult cmp = [strongSelf->sqliteVersion compare:@"3.8.8.2" options:NSNumericSearch];
|
|
if (cmp != NSOrderedAscending)
|
|
{
|
|
checkpointMode = SQLITE_CHECKPOINT_TRUNCATE;
|
|
}
|
|
|
|
#endif
|
|
|
|
checkpointResult = sqlite3_wal_checkpoint_v2(db, "main", checkpointMode,
|
|
&totalFrameCount, &checkpointedFrameCount);
|
|
|
|
YDBLogInfo(@"Post-checkpoint: src(c) mode(%@) result(%d) frames(%d) checkpointed(%d)",
|
|
(checkpointMode == SQLITE_CHECKPOINT_RESTART ? @"restart" : @"truncate"),
|
|
checkpointResult, totalFrameCount, checkpointedFrameCount);
|
|
|
|
if (checkpointResult == SQLITE_OK)
|
|
{
|
|
if (checkpointMode == SQLITE_CHECKPOINT_RESTART)
|
|
{
|
|
// Write something to the database to force restart the WAL.
|
|
// We're just going to set a random value in the yap2 table.
|
|
|
|
NSString *uuid = [[NSUUID UUID] UUIDString];
|
|
|
|
[self beginTransaction];
|
|
|
|
int status;
|
|
sqlite3_stmt *statement;
|
|
|
|
char *stmt = "INSERT OR REPLACE INTO \"yap2\" (\"extension\", \"key\", \"data\") VALUES (?, ?, ?);";
|
|
|
|
int const bind_extension = SQLITE_BIND_START + 0;
|
|
int const bind_key = SQLITE_BIND_START + 1;
|
|
int const bind_data = SQLITE_BIND_START + 2;
|
|
|
|
status = sqlite3_prepare_v2(db, stmt, (int)strlen(stmt)+1, &statement, NULL);
|
|
if (status != SQLITE_OK)
|
|
{
|
|
YDBLogError(@"%@: Error creating statement: %d %s", THIS_METHOD, status, sqlite3_errmsg(db));
|
|
}
|
|
else
|
|
{
|
|
char *extension = "";
|
|
sqlite3_bind_text(statement, bind_extension, extension, (int)strlen(extension), SQLITE_STATIC);
|
|
|
|
char *key = "random";
|
|
sqlite3_bind_text(statement, bind_key, key, (int)strlen(key), SQLITE_STATIC);
|
|
|
|
YapDatabaseString _uuid; MakeYapDatabaseString(&_uuid, uuid);
|
|
sqlite3_bind_text(statement, bind_data, _uuid.str, _uuid.length, SQLITE_STATIC);
|
|
|
|
status = sqlite3_step(statement);
|
|
if (status != SQLITE_DONE)
|
|
{
|
|
YDBLogError(@"%@: Error in statement: %d %s", THIS_METHOD, status, sqlite3_errmsg(db));
|
|
}
|
|
|
|
sqlite3_finalize(statement);
|
|
FreeYapDatabaseString(&_uuid);
|
|
}
|
|
|
|
[self commitTransaction];
|
|
}
|
|
|
|
atomic_store(&aggressiveCheckpointEnabled, false);
|
|
}
|
|
}
|
|
|
|
- (BOOL)tryResetLongLivedReadTransactions
|
|
{
|
|
NSAssert(dispatch_get_specific(IsOnWriteQueueKey), @"Must go through writeQueue.");
|
|
|
|
__block NSMutableArray<YapDatabaseConnection *> *strongConnections = nil;
|
|
__block dispatch_group_t group = NULL;
|
|
|
|
__block YAPUnfairLock spinLock = YAP_UNFAIR_LOCK_INIT;
|
|
__block atomic_bool hasWriteQueue = true;
|
|
|
|
dispatch_sync(snapshotQueue, ^{ @autoreleasepool {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
|
|
|
for (YapDatabaseConnectionState *state in connectionStates)
|
|
{
|
|
if (state->activeReadTransaction && state->longLivedReadTransaction)
|
|
{
|
|
// Create strong reference (state->connection is weak)
|
|
__strong YapDatabaseConnection *connection = state->connection;
|
|
|
|
if (connection)
|
|
{
|
|
if (strongConnections == nil)
|
|
strongConnections = [NSMutableArray array];
|
|
|
|
[strongConnections addObject:connection];
|
|
|
|
if (group == NULL)
|
|
group = dispatch_group_create();
|
|
|
|
dispatch_group_async(group, connection->connectionQueue, ^{
|
|
|
|
YAPUnfairLockLock(&spinLock);
|
|
{
|
|
if (atomic_load(&hasWriteQueue))
|
|
{
|
|
[connection resetLongLivedReadTransaction];
|
|
}
|
|
}
|
|
YAPUnfairLockUnlock(&spinLock);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma clang diagnostic pop
|
|
}});
|
|
|
|
if (strongConnections)
|
|
{
|
|
// Edge case protection:
|
|
// Bug fix for issues: #437, #441
|
|
//
|
|
// Deadlock crash if:
|
|
// - YapDatabase is the last one holding a strong reference to a YapDatabaseConnection instance
|
|
// - The [connection dealloc] call occurs within the snapshotQueue
|
|
//
|
|
// This is a workaround to ensure that the dealloc occurs outside the snapshotQueue.
|
|
//
|
|
[strongConnections removeAllObjects];
|
|
}
|
|
|
|
// dispatch_group_wait():
|
|
// Returns zero on success (all blocks associated with the group completed before the specified timeout)
|
|
// or non-zero on error (timeout occurred).
|
|
//
|
|
long ready = 0;
|
|
if (group) {
|
|
ready = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(50 * NSEC_PER_MSEC)));
|
|
}
|
|
|
|
if (ready != 0)
|
|
{
|
|
YAPUnfairLockLock(&spinLock);
|
|
{
|
|
atomic_store(&hasWriteQueue, false);
|
|
}
|
|
YAPUnfairLockUnlock(&spinLock);
|
|
|
|
return NO;
|
|
}
|
|
else
|
|
{
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Consulted by YapDatabaseConnection after performing a read-write transaction.
|
|
*
|
|
* When aggressive checkpointing is triggered,
|
|
* the connections will perform a checkpoint after every read-write transaction.
|
|
**/
|
|
- (BOOL)aggressiveCheckpointEnabled
|
|
{
|
|
return atomic_load(&aggressiveCheckpointEnabled);
|
|
}
|
|
|
|
- (void)noteCheckpointWithTotalFrames:(int)totalFrameCount checkpointedFrames:(int)checkpointedFrameCount
|
|
{
|
|
uint64_t walApproximateFileSize = totalFrameCount * pageSize;
|
|
|
|
if (walApproximateFileSize < options.aggressiveWALTruncationSize)
|
|
{
|
|
atomic_store(&aggressiveCheckpointEnabled, false);
|
|
}
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
|
|
// This method is only used by tests.
|
|
- (void)flushInternalQueue
|
|
{
|
|
dispatch_sync(internalQueue,
|
|
^{
|
|
});
|
|
}
|
|
|
|
// This method is only used by tests.
|
|
- (void)flushCheckpointQueue
|
|
{
|
|
dispatch_sync(checkpointQueue,
|
|
^{
|
|
});
|
|
}
|
|
|
|
#endif
|
|
|
|
@end
|