// // RSDatabaseQueue.m // RSDatabase // // Created by Brent Simmons on 10/19/13. // Copyright (c) 2013 Ranchero Software, LLC. All rights reserved. // #import "RSDatabaseQueue.h" #import // This has been deprecated — use DatabaseQueue instead. @interface RSDatabaseQueue () @property (nonatomic, strong, readwrite) NSString *databasePath; @property (nonatomic, assign) BOOL excludeFromBackup; @property (nonatomic, strong, readonly) dispatch_queue_t serialDispatchQueue; @property (nonatomic) BOOL closing; @property (nonatomic) BOOL closed; @end @implementation RSDatabaseQueue #pragma mark - Init - (instancetype)initWithFilepath:(NSString *)filepath excludeFromBackup:(BOOL)excludeFromBackup { self = [super init]; if (self == nil) return self; _databasePath = filepath; _serialDispatchQueue = dispatch_queue_create([[NSString stringWithFormat:@"RSDatabaseQueue serial queue - %@", filepath.lastPathComponent] UTF8String], DISPATCH_QUEUE_SERIAL); _excludeFromBackup = excludeFromBackup; return self; } #pragma mark - Database - (FMDatabase *)database { /*I've always done it this way -- kept a per-thread database in the threadDictionary -- and I know it's solid. Maybe it's not necessary with a serial queue, but my understanding was that SQLite wanted a different database per thread (and a serial queue may run on different threads).*/ if (self.closed) { return nil; } NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary]; FMDatabase *database = threadDictionary[self.databasePath]; if (!database || !database.open) { database = [FMDatabase databaseWithPath:self.databasePath]; [database open]; [database executeUpdate:@"PRAGMA synchronous = 1;"]; [database setShouldCacheStatements:YES]; if ([self.delegate respondsToSelector:@selector(makeFunctionsForDatabase:queue:)]) { [self.delegate makeFunctionsForDatabase:database queue:self]; } threadDictionary[self.databasePath] = database; if (self.excludeFromBackup) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSURL *URL = [NSURL fileURLWithPath:self.databasePath isDirectory:NO]; NSError *error = nil; [URL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&error]; }); } } return database; } #pragma mark - API - (void)createTablesUsingStatements:(NSString *)createStatements { [self runInDatabase:^(FMDatabase *database) { [self runCreateStatements:createStatements database:database]; }]; } - (void)createTablesUsingStatementsSync:(NSString *)createStatements { [self runInDatabaseSync:^(FMDatabase *database) { [self runCreateStatements:createStatements database:database]; }]; } - (void)runCreateStatements:(NSString *)createStatements database:(FMDatabase *)database { [createStatements enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) { if ([line.lowercaseString hasPrefix:@"create "]) { [database executeUpdate:line]; } *stop = NO; }]; } - (void)update:(RSDatabaseBlock)updateBlock { dispatch_async(self.serialDispatchQueue, ^{ [self runInTransaction:updateBlock]; }); } - (void)updateSync:(RSDatabaseBlock)updateBlock { dispatch_sync(self.serialDispatchQueue, ^{ [self runInTransaction:updateBlock]; }); } - (void)runInTransaction:(RSDatabaseBlock)databaseBlock { @autoreleasepool { FMDatabase *database = [self database]; [database beginTransaction]; databaseBlock(database); [database commit]; } } - (void)runInDatabase:(RSDatabaseBlock)databaseBlock { dispatch_async(self.serialDispatchQueue, ^{ @autoreleasepool { databaseBlock([self database]); } }); } - (void)runInDatabaseSync:(RSDatabaseBlock)databaseBlock { dispatch_sync(self.serialDispatchQueue, ^{ @autoreleasepool { databaseBlock([self database]); } }); } - (void)fetch:(RSDatabaseBlock)fetchBlock { [self runInDatabase:fetchBlock]; } - (void)fetchSync:(RSDatabaseBlock)fetchBlock { dispatch_sync(self.serialDispatchQueue, ^{ @autoreleasepool { fetchBlock([self database]); } }); } - (void)vacuum { dispatch_async(self.serialDispatchQueue, ^{ @autoreleasepool { [[self database] executeUpdate:@"vacuum;"]; } }); } - (void)vacuumIfNeeded { NSTimeInterval interval = (24 * 60 * 60) * 6; // 6 days [self vacuumIfNeeded:@"lastVacuumDate" intervalBetweenVacuums:interval]; } - (void)vacuumIfNeeded:(NSString *)defaultsKey intervalBetweenVacuums:(NSTimeInterval)intervalBetweenVacuums { NSDate *lastVacuumDate = [[NSUserDefaults standardUserDefaults] objectForKey:defaultsKey]; if (!lastVacuumDate || ![lastVacuumDate isKindOfClass:[NSDate class]]) { [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:defaultsKey]; return; } NSDate *cutoffDate = [[NSDate date] dateByAddingTimeInterval: -(intervalBetweenVacuums)]; if ([cutoffDate earlierDate:lastVacuumDate] == lastVacuumDate) { [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:defaultsKey]; [self vacuum]; } } - (NSArray *)arrayWithSingleColumnResultSet:(FMResultSet *)rs { NSMutableArray *results = [NSMutableArray new]; while ([rs next]) { id oneObject = [rs objectForColumnIndex:0]; if (oneObject) { [results addObject:oneObject]; } } return [results copy]; } - (void)close { self.closing = YES; [self runInDatabaseSync:^(FMDatabase *database) { self.closed = YES; [database close]; }]; } @end