/
WMDatabase.m
192 lines (156 loc) 路 5.52 KB
/
WMDatabase.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#import "WMDatabase.h"
#import <sqlite3.h>
@implementation WMDatabase {
NSString *_path;
}
- (instancetype) initWithPath:(NSString *)path
{
if (self = [super init]) {
_path = path;
_fmdb = [FMDatabase databaseWithPath:path];
[self open];
}
return self;
}
+ (instancetype) databaseWithPath:(NSString *)path
{
return [[self alloc] initWithPath:path];
}
- (void) open
{
if (![_fmdb open]) {
[NSException raise:@"OpenFailed" format:@"Failed to open the database: %@", _fmdb.lastErrorMessage];
}
// TODO: Experiment with WAL
// // must be queryRaw - returns value
// _ = try queryRaw("pragma journal_mode=wal")
// TODO: Configurable logger
NSLog(@"Opened database at %@", _path);
}
#pragma mark - Executing queries
- (BOOL) executeQuery:(NSString *)query args:(NSArray *)args error:(NSError **)errorPtr
{
return [_fmdb executeUpdate:query values:args error:errorPtr];
}
- (BOOL) executeStatements:(NSString *)sql error:(NSError **)errorPtr
{
if (![_fmdb executeStatements:sql]) {
*errorPtr = _fmdb.lastError;
return NO;
}
return YES;
}
- (FMResultSet *) queryRaw:(NSString *)query args:(NSArray *)args error:(NSError **)errorPtr
{
return [_fmdb executeQuery:query values:args error:errorPtr];
}
- (NSNumber * _Nullable) count:(NSString *)query args:(NSArray *)args error:(NSError **)errorPtr
{
FMResultSet *result = [_fmdb executeQuery:query values:args error:errorPtr];
if (!result) {
*errorPtr = _fmdb.lastError;
return nil;
}
if (![result next]) {
*errorPtr = [NSError errorWithDomain:@"WMDatabase" code:0 userInfo:@{
NSLocalizedDescriptionKey: @"Invalid count query, can't find next() on the result"
}];
return nil;
}
if ([result columnIndexForName:@"count"] != 1) {
*errorPtr = [NSError errorWithDomain:@"WMDatabase" code:0 userInfo:@{
NSLocalizedDescriptionKey: @"Invalid count query, can't find `count` column"
}];
return nil;
}
return @([result intForColumn:@"count"]);
}
#pragma mark - Other database functions
- (BOOL) inTransaction:(BOOL (^)(NSError**))transactionBlock error:(NSError**)errorPtr
{
if (![_fmdb beginTransaction]) {
*errorPtr = _fmdb.lastError;
return NO;
}
BOOL txnResult = transactionBlock(errorPtr);
if (txnResult) {
if (![_fmdb commit]) {
*errorPtr = _fmdb.lastError;
return NO;
}
return YES;
} else {
if (![_fmdb rollback]) {
*errorPtr = _fmdb.lastError;
}
return NO;
}
}
- (long) userVersion
{
FMResultSet *result = [_fmdb executeQuery:@"pragma user_version"];
[result next];
return [result longForColumnIndex:0];
}
- (void) setUserVersion:(long)userVersion
{
BOOL result = [_fmdb executeUpdateWithFormat:@"pragma user_version = %li", userVersion];
if (!result) {
[NSException raise:@"SetUserVersionFailed" format:@"Failed to set user version: %@", _fmdb.lastErrorMessage];
}
}
- (BOOL) unsafeDestroyEverything:(NSError**)errorPtr
{
// NOTE: Deleting files by default because it seems simpler, more reliable
// But sadly this won't work for in-memory (shared) databases
if ([self isInMemoryDatabase]) {
// NOTE: As of iOS 14, selecting tables from sqlite_master and deleting them does not work
// They seem to be enabling "defensive" config. So we use another obscure method to clear the database
// https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigresetdatabase
if (sqlite3_db_config(_fmdb.sqliteHandle, SQLITE_DBCONFIG_RESET_DATABASE, 1, 0) != SQLITE_OK) {
*errorPtr = [NSError errorWithDomain:@"WMDatabase" code:0 userInfo:@{
NSLocalizedDescriptionKey: @"Failed to enable reset database mode",
@"FMDBError": _fmdb.lastError
}];
return NO;
}
if (![self executeStatements:@"vacuum" error:errorPtr]) {
return NO;
}
if (sqlite3_db_config(_fmdb.sqliteHandle, SQLITE_DBCONFIG_RESET_DATABASE, 0, 0) != SQLITE_OK) {
*errorPtr = [NSError errorWithDomain:@"WMDatabase" code:0 userInfo:@{
NSLocalizedDescriptionKey: @"Failed to disable reset database mode",
@"FMDBError": _fmdb.lastError
}];
return NO;
}
return YES;
} else {
if (![_fmdb close]) {
*errorPtr = [NSError errorWithDomain:@"WMDatabase" code:0 userInfo:@{
NSLocalizedDescriptionKey: @"Could not close database",
@"FMDBError": _fmdb.lastError
}];
return NO;
}
NSFileManager *manager = [NSFileManager defaultManager];
// remove database
if (![manager removeItemAtPath:_path error:errorPtr]) {
return NO;
}
// try removing database WAL files (ignore errors)
[manager removeItemAtPath:[NSString stringWithFormat:@"%@-wal", _path] error:nil];
[manager removeItemAtPath:[NSString stringWithFormat:@"%@-shm", _path] error:nil];
// reopen database
[self open];
return YES;
}
}
# pragma mark - Private helpers
- (BOOL) isInMemoryDatabase
{
return [_path isEqualToString:@":memory:"]
|| [_path isEqualToString:@"file::memory:"]
|| [_path containsString:@"?mode=memory"];
}
@end