Explorar el Código

Various database browser related upgrades

- All database managers automatically open and close connections to the underlying database
- Allow getting last result and last rowid from FLEXSQLiteDatabaseManager
- Execute statements with arguments with FLEXSQLiteDatabaseManager
Tanner Bennett hace 6 años
padre
commit
d9e9be53d8

+ 2 - 2
Classes/GlobalStateExplorers/DatabaseBrowser/FLEXDatabaseManager.h

@@ -14,14 +14,14 @@
 #import <Foundation/Foundation.h>
 #import "FLEXSQLResult.h"
 
+/// Conformers should automatically open and close the database
 @protocol FLEXDatabaseManager <NSObject>
 
 @required
 
+/// @return \c nil if the database couldn't be opened
 + (instancetype)managerForDatabase:(NSString *)path;
 
-- (BOOL)open;
-
 /// @return a list of all table names
 - (NSArray<NSString *> *)queryAllTables;
 - (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName;

+ 5 - 1
Classes/GlobalStateExplorers/DatabaseBrowser/FLEXRealmDatabaseManager.m

@@ -20,7 +20,7 @@
 @interface FLEXRealmDatabaseManager ()
 
 @property (nonatomic, copy) NSString *path;
-@property (nonatomic) RLMRealm * realm;
+@property (nonatomic) RLMRealm *realm;
 
 @end
 
@@ -43,6 +43,10 @@ static Class RLMRealmClass = nil;
     self = [super init];
     if (self) {
         _path = path;
+        
+        if (![self open]) {
+            return nil;
+        }
     }
     
     return self;

+ 7 - 0
Classes/GlobalStateExplorers/DatabaseBrowser/FLEXSQLResult.h

@@ -14,6 +14,9 @@ NS_ASSUME_NONNULL_BEGIN
 
 /// Describes the result of a non-select query, or an error of any kind of query
 + (instancetype)message:(NSString *)message;
+/// Describes the result of a known failed execution
++ (instancetype)error:(NSString *)message;
+
 /// @param rowData A list of rows, where each element in the row
 /// corresponds to the column given in /c columnNames
 + (instancetype)columns:(NSArray<NSString *> *)columnNames
@@ -21,6 +24,10 @@ NS_ASSUME_NONNULL_BEGIN
 
 @property (nonatomic, readonly, nullable) NSString *message;
 
+/// A value of YES means this is surely an error,
+/// but it still might be an error even with a value of NO
+@property (nonatomic, readonly) BOOL isError;
+
 /// A list of column names
 @property (nonatomic, readonly, nullable) NSArray<NSString *> *columns;
 /// A list of rows, where each element in the row corresponds

+ 6 - 0
Classes/GlobalStateExplorers/DatabaseBrowser/FLEXSQLResult.m

@@ -16,6 +16,12 @@
     return [[self alloc] initWithmessage:message columns:nil rows:nil];
 }
 
++ (instancetype)error:(NSString *)message {
+    FLEXSQLResult *result = [self message:message];
+    result->_isError = YES;
+    return result;
+}
+
 + (instancetype)columns:(NSArray<NSString *> *)columnNames rows:(NSArray<NSArray<NSString *> *> *)rowData {
     return [[self alloc] initWithmessage:nil columns:columnNames rows:rowData];
 }

+ 13 - 0
Classes/GlobalStateExplorers/DatabaseBrowser/FLEXSQLiteDatabaseManager.h

@@ -13,7 +13,20 @@
 
 #import <Foundation/Foundation.h>
 #import "FLEXDatabaseManager.h"
+#import "FLEXSQLResult.h"
 
 @interface FLEXSQLiteDatabaseManager : NSObject <FLEXDatabaseManager>
 
+/// Contains the result of the last operation, which may be an error
+@property (nonatomic, readonly) FLEXSQLResult *lastResult;
+/// Calls into \c sqlite3_last_insert_rowid()
+@property (nonatomic, readonly) NSInteger lastRowID;
+
+/// Given a statement like 'SELECT * from @table where @col = @val' and arguments
+/// like { @"table": @"Album", @"col": @"year", @"val" @1 } this method will
+/// invoke the statement and properly bind the given arguments to the statement.
+///
+/// You may pass NSStrings, NSData, NSNumbers, or NSNulls as values.
+- (FLEXSQLResult *)executeStatement:(NSString *)statement arguments:(NSDictionary<NSString *, id> *)args;
+
 @end

+ 110 - 11
Classes/GlobalStateExplorers/DatabaseBrowser/FLEXSQLiteDatabaseManager.m

@@ -9,7 +9,7 @@
 #import "FLEXSQLiteDatabaseManager.h"
 #import "FLEXManager.h"
 #import "NSArray+Functional.h"
-#import "FLEXSQLResult.h"
+#import "FLEXRuntimeConstants.h"
 #import <sqlite3.h>
 
 static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
@@ -36,6 +36,10 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
     return self;
 }
 
+- (void)dealloc {
+    [self close];
+}
+
 - (BOOL)open {
     if (self.db) {
         return YES;
@@ -52,8 +56,7 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
 #endif
 
     if (err != SQLITE_OK) {
-        NSLog(@"error opening!: %d", err);
-        return NO;
+        return [self storeErrorForLastTask:@"Open"];
     }
     
     return YES;
@@ -81,7 +84,9 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
                 }
             }
         } else if (SQLITE_OK != rc) {
-            NSLog(@"error closing!: %d", rc);
+            [self storeErrorForLastTask:@"Close"];
+            self.db = nil;
+            return NO;
         }
     } while (retry);
     
@@ -89,6 +94,10 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
     return YES;
 }
 
+- (NSInteger)lastRowID {
+    return (NSInteger)sqlite3_last_insert_rowid(self.db);
+}
+
 - (NSArray<NSString *> *)queryAllTables {
     return [[self executeStatement:QUERY_TABLENAMES].rows flex_mapped:^id(NSArray *table, NSUInteger idx) {
         return table.firstObject;
@@ -111,14 +120,24 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
 }
 
 - (FLEXSQLResult *)executeStatement:(NSString *)sql {
+    return [self executeStatement:sql arguments:nil];
+}
+
+- (FLEXSQLResult *)executeStatement:(NSString *)sql arguments:(NSDictionary *)args {
     [self open];
     
     FLEXSQLResult *result = nil;
     
     sqlite3_stmt *pstmt;
-    if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0) == SQLITE_OK) {
+    int status;
+    if ((status = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0)) == SQLITE_OK) {
         NSMutableArray<NSArray *> *rows = [NSMutableArray new];
         
+        // Bind parameters, if any
+        if (![self bindParameters:args toStatement:pstmt]) {
+            return self.lastResult;
+        }
+        
         // Grab columns
         int columnCount = sqlite3_column_count(pstmt);
         NSArray<NSString *> *columns = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
@@ -126,7 +145,6 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
         }];
         
         // Execute statement
-        int status;
         while ((status = sqlite3_step(pstmt)) == SQLITE_ROW) {
             // Grab rows if this is a selection query
             int dataCount = sqlite3_data_count(pstmt);
@@ -140,30 +158,111 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
         if (status == SQLITE_DONE) {
             if (rows.count) {
                 // We selected some rows
-                result = [FLEXSQLResult columns:columns rows:rows];
+                result = _lastResult = [FLEXSQLResult columns:columns rows:rows];
             } else {
                 // We executed a query like INSERT, UDPATE, or DELETE
                 int rowsAffected = sqlite3_changes(_db);
                 NSString *message = [NSString stringWithFormat:@"%d row(s) affected", rowsAffected];
-                result = [FLEXSQLResult message:message];
+                result = _lastResult = [FLEXSQLResult message:message];
             }
         } else {
             // An error occured executing the query
-            result = [FLEXSQLResult message:@(sqlite3_errmsg(_db) ?: "(Execution: empty error)")];
+            result = _lastResult = [self errorResult:@"Execution"];
         }
     } else {
         // An error occurred creating the prepared statement
-        result = [FLEXSQLResult message:@(sqlite3_errmsg(_db) ?: "(Prepared statement: empty error)")];
+        result = _lastResult = [self errorResult:@"Prepared statement"];
     }
     
     sqlite3_finalize(pstmt);
-    [self close];
     return result;
 }
 
 
 #pragma mark - Private
 
+/// @return YES on success, NO if an error was encountered and stored in \c lastResult
+- (BOOL)bindParameters:(NSDictionary *)args toStatement:(sqlite3_stmt *)pstmt {
+    for (NSString *param in args.allKeys) {
+        int status = SQLITE_OK, idx = sqlite3_bind_parameter_index(pstmt, param.UTF8String);
+        id value = args[param];
+        
+        if (idx == 0) {
+            // No parameter matching that arg
+            @throw NSInternalInconsistencyException;
+        }
+        
+        // Null
+        if ([value isKindOfClass:[NSNull class]]) {
+            status = sqlite3_bind_null(pstmt, idx);
+        }
+        // String params
+        else if ([value isKindOfClass:[NSString class]]) {
+            const char *str = [value UTF8String];
+            status = sqlite3_bind_text(pstmt, idx, str, (int)strlen(str), SQLITE_TRANSIENT);
+        }
+        // Data params
+        else if ([value isKindOfClass:[NSData class]]) {
+            const void *blob = [value bytes];
+            status = sqlite3_bind_blob64(pstmt, idx, blob, [value length], SQLITE_TRANSIENT);
+        }
+        // Primitive params
+        else if ([value isKindOfClass:[NSNumber class]]) {
+            FLEXTypeEncoding type = [value objCType][0];
+            switch (type) {
+                case FLEXTypeEncodingCBool:
+                case FLEXTypeEncodingChar:
+                case FLEXTypeEncodingUnsignedChar:
+                case FLEXTypeEncodingShort:
+                case FLEXTypeEncodingUnsignedShort:
+                case FLEXTypeEncodingInt:
+                case FLEXTypeEncodingUnsignedInt:
+                case FLEXTypeEncodingLong:
+                case FLEXTypeEncodingUnsignedLong:
+                case FLEXTypeEncodingLongLong:
+                case FLEXTypeEncodingUnsignedLongLong:
+                    status = sqlite3_bind_int64(pstmt, idx, (sqlite3_int64)[value longValue]);
+                    break;
+                
+                case FLEXTypeEncodingFloat:
+                case FLEXTypeEncodingDouble:
+                    status = sqlite3_bind_double(pstmt, idx, [value doubleValue]);
+                    break;
+                    
+                default:
+                    @throw NSInternalInconsistencyException;
+                    break;
+            }
+        }
+        // Unsupported type
+        else {
+            @throw NSInternalInconsistencyException;
+        }
+        
+        if (status != SQLITE_OK) {
+            return [self storeErrorForLastTask:
+                [NSString stringWithFormat:@"Binding param named '%@'", param]
+            ];
+        }
+    }
+    
+    return YES;
+}
+
+- (BOOL)storeErrorForLastTask:(NSString *)action {
+    _lastResult = [self errorResult:action];
+    return NO;
+}
+
+- (FLEXSQLResult *)errorResult:(NSString *)description {
+    const char *error = sqlite3_errmsg(_db);
+    NSString *message = error ? @(error) : [NSString
+        stringWithFormat:@"(%@: empty error", description
+    ];
+    
+    return [FLEXSQLResult error:message];
+}
+
 - (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt {
     int columnType = sqlite3_column_type(stmt, columnIdx);