Skip to content

Added ability to run sql queries on sqlite files #294

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 3, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -12,16 +12,23 @@
// which Flying Meat Inc. licenses this file to you.

#import <Foundation/Foundation.h>
#import "FLEXSQLResult.h"

@protocol FLEXDatabaseManager <NSObject>

@required

+ (instancetype)managerForDatabase:(NSString *)path;

- (BOOL)open;

/// @return a list of all table names
- (NSArray<NSString *> *)queryAllTables;
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName;
- (NSArray<NSArray *> *)queryAllDataWithTableName:(NSString *)tableName;
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName;
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName;

@optional

- (FLEXSQLResult *)executeStatement:(NSString *)SQLStatement;

@end
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@

#import "FLEXRealmDatabaseManager.h"
#import "NSArray+Functional.h"
#import "FLEXSQLResult.h"

#if __has_include(<Realm/Realm.h>)
#import <Realm/Realm.h>
@@ -68,15 +69,15 @@ - (BOOL)open {
}];
}

- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName {
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName];
// Map each column to its name
return [objectSchema.properties flex_mapped:^id(RLMProperty *property, NSUInteger idx) {
return property.name;
}];
}

- (NSArray<NSArray *> *)queryAllDataWithTableName:(NSString *)tableName {
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName];
RLMResults *results = [self.realm allObjects:tableName];
if (results.count == 0 || !objectSchema) {
41 changes: 41 additions & 0 deletions Classes/GlobalStateExplorers/DatabaseBrowser/FLEXSQLResult.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// FLEXSQLResult.h
// FLEX
//
// Created by Tanner on 3/3/20.
// Copyright © 2020 Flipboard. All rights reserved.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FLEXSQLResult : NSObject

/// Describes the result of a non-select query, or an error of any kind of query
+ (instancetype)message:(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
rows:(NSArray<NSArray<NSString *> *> *)rowData;

@property (nonatomic, readonly, nullable) NSString *message;

/// A list of column names
@property (nonatomic, readonly, nullable) NSArray<NSString *> *columns;
/// A list of rows, where each element in the row corresponds
/// to the value of the column at the same index in \c columns.
///
/// That is, given a row, looping over the contents of the row and
/// the contents of \c columns will give you key-value pairs of
/// column names to column values for that row.
@property (nonatomic, readonly, nullable) NSArray<NSArray<NSString *> *> *rows;
/// A list of rows where the fields are paired to column names.
///
/// This property is lazily constructed by looping over
/// the rows and columns present in the other two properties.
@property (nonatomic, readonly, nullable) NSArray<NSDictionary<NSString *, id> *> *keyedRows;

@end

NS_ASSUME_NONNULL_END
47 changes: 47 additions & 0 deletions Classes/GlobalStateExplorers/DatabaseBrowser/FLEXSQLResult.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// FLEXSQLResult.m
// FLEX
//
// Created by Tanner on 3/3/20.
// Copyright © 2020 Flipboard. All rights reserved.
//

#import "FLEXSQLResult.h"
#import "NSArray+Functional.h"

@implementation FLEXSQLResult
@synthesize keyedRows = _keyedRows;

+ (instancetype)message:(NSString *)message {
return [[self alloc] initWithmessage:message columns:nil rows:nil];
}

+ (instancetype)columns:(NSArray<NSString *> *)columnNames rows:(NSArray<NSArray<NSString *> *> *)rowData {
return [[self alloc] initWithmessage:nil columns:columnNames rows:rowData];
}

- (id)initWithmessage:(NSString *)message columns:(NSArray *)columns rows:(NSArray<NSArray *> *)rows {
NSParameterAssert(message || (columns && rows));
NSParameterAssert(columns.count == rows.firstObject.count);

self = [super init];
if (self) {
_message = message;
_columns = columns;
_rows = rows;
}

return self;
}

- (NSArray<NSDictionary<NSString *,id> *> *)keyedRows {
if (!_keyedRows) {
_keyedRows = [self.rows flex_mapped:^id(NSArray<NSString *> *row, NSUInteger idx) {
return [NSDictionary dictionaryWithObjects:row forKeys:self.columns];
}];
}

return _keyedRows;
}

@end
Original file line number Diff line number Diff line change
@@ -9,12 +9,13 @@
#import "FLEXSQLiteDatabaseManager.h"
#import "FLEXManager.h"
#import "NSArray+Functional.h"
#import "FLEXSQLResult.h"
#import <sqlite3.h>

static NSString * const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";

@interface FLEXSQLiteDatabaseManager ()
@property (nonatomic, readonly) sqlite3 *db;
@property (nonatomic) sqlite3 *db;
@property (nonatomic, copy) NSString *path;
@end

@@ -36,7 +37,7 @@ - (instancetype)initWithPath:(NSString *)path {
}

- (BOOL)open {
if (_db) {
if (self.db) {
return YES;
}

@@ -57,9 +58,9 @@ - (BOOL)open {

return YES;
}

- (BOOL)close {
if (!_db) {
if (!self.db) {
return YES;
}

@@ -84,88 +85,85 @@ - (BOOL)close {
}
} while (retry);

_db = nil;
self.db = nil;
return YES;
}

- (NSArray<NSString *> *)queryAllTables {
return [[self executeQuery:QUERY_TABLENAMES_SQL] flex_mapped:^id(NSArray *table, NSUInteger idx) {
return [[self executeStatement:QUERY_TABLENAMES].rows flex_mapped:^id(NSArray *table, NSUInteger idx) {
return table.firstObject;
}];
}

- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName {
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
NSArray<NSDictionary *> *results = [self executeQueryWithColumns:sql];
FLEXSQLResult *results = [self executeStatement:sql];

return [results flex_mapped:^id(NSDictionary *column, NSUInteger idx) {
return [results.keyedRows flex_mapped:^id(NSDictionary *column, NSUInteger idx) {
return column[@"name"];
}];
}

- (NSArray<NSArray *> *)queryAllDataWithTableName:(NSString *)tableName {
return [self executeQuery:[@"SELECT * FROM "
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
return [self executeStatement:[@"SELECT * FROM "
stringByAppendingString:tableName
]];
]].rows;
}

#pragma mark - Private

/// @return an array of rows, where each row is an array
/// containing the values of each column for that row
- (NSArray<NSArray *> *)executeQuery:(NSString *)sql {
- (FLEXSQLResult *)executeStatement:(NSString *)sql {
[self open];

NSMutableArray<NSArray *> *results = [NSMutableArray array];
FLEXSQLResult *result = nil;

sqlite3_stmt *pstmt;
if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0) == SQLITE_OK) {
while (sqlite3_step(pstmt) == SQLITE_ROW) {
int num_cols = sqlite3_data_count(pstmt);
if (num_cols > 0) {
int columnCount = sqlite3_column_count(pstmt);

[results addObject:[NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
NSMutableArray<NSArray *> *rows = [NSMutableArray new];

// Grab columns
int columnCount = sqlite3_column_count(pstmt);
NSArray<NSString *> *columns = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
return @(sqlite3_column_name(pstmt, (int)i));
}];

// 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);
if (dataCount > 0) {
[rows addObject:[NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
return [self objectForColumnIndex:(int)i stmt:pstmt];
}]];
}
}
}

[self close];
return results;
}

/// Like \c executeQuery: except that a list of dictionaries are returned,
/// where the keys are column names and the values are the data.
- (NSArray<NSDictionary *> *)executeQueryWithColumns:(NSString *)sql {
[self open];

NSMutableArray<NSDictionary *> *results = [NSMutableArray array];

sqlite3_stmt *pstmt;
if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0) == SQLITE_OK) {
while (sqlite3_step(pstmt) == SQLITE_ROW) {
int num_cols = sqlite3_data_count(pstmt);
if (num_cols > 0) {
int columnCount = sqlite3_column_count(pstmt);


NSMutableDictionary *rowFields = [NSMutableDictionary new];
for (int i = 0; i < columnCount; i++) {
id value = [self objectForColumnIndex:(int)i stmt:pstmt];
rowFields[@(sqlite3_column_name(pstmt, i))] = value;
}

[results addObject:rowFields];

if (status == SQLITE_DONE) {
if (rows.count) {
// We selected some rows
result = [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];
}
} else {
// An error occured executing the query
result = [FLEXSQLResult message:@(sqlite3_errmsg(_db) ?: "(Execution: empty error)")];
}
} else {
// An error occurred creating the prepared statement
result = [FLEXSQLResult message:@(sqlite3_errmsg(_db) ?: "(Prepared statement: empty error)")];
}

sqlite3_finalize(pstmt);
[self close];
return results;
return result;
}


#pragma mark - Private

- (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt {
int columnType = sqlite3_column_type(stmt, columnIdx);

@@ -184,7 +182,7 @@ - (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt {
return [self stringForColumnIndex:columnIdx stmt:stmt] ?: NSNull.null;
}
}

- (NSString *)stringForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt {
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || columnIdx < 0) {
return nil;
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@

@interface FLEXTableContentViewController : UIViewController

@property (nonatomic) NSArray<NSString *> *columns;
@property (nonatomic) NSArray<NSArray *> *rows;
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData;

@end
Original file line number Diff line number Diff line change
@@ -14,21 +14,32 @@
@interface FLEXTableContentViewController () <
FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate
>
@property (nonatomic, readonly) NSArray<NSString *> *columns;
@property (nonatomic, copy) NSArray<NSArray *> *rows;

@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
@end

@implementation FLEXTableContentViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.edgesForExtendedLayout = UIRectEdgeNone;

+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData {
FLEXTableContentViewController *controller = [self new];
controller->_columns = columnNames;
controller->_rows = rowData;
return controller;
}

- (void)loadView {
[super loadView];

[self.view addSubview:self.multiColumnView];
}

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
- (void)viewDidLoad {
[super viewDidLoad];

self.edgesForExtendedLayout = UIRectEdgeNone;
[self.multiColumnView reloadData];
}

@@ -38,8 +49,8 @@ - (FLEXMultiColumnTableView *)multiColumnView {
initWithFrame:FLEXRectSetSize(CGRectZero, self.view.frame.size)
];

_multiColumnView.dataSource = self;
_multiColumnView.delegate = self;
_multiColumnView.dataSource = self;
_multiColumnView.delegate = self;
}

return _multiColumnView;
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@
#import "FLEXRealmDatabaseManager.h"
#import "FLEXTableContentViewController.h"
#import "NSArray+Functional.h"
#import "FLEXAlert.h"

@interface FLEXTableListViewController ()
@property (nonatomic, readonly) id<FLEXDatabaseManager> dbm;
@@ -30,14 +31,53 @@ @implementation FLEXTableListViewController
- (instancetype)initWithPath:(NSString *)path {
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
_path = [path copy];
_dbm = [self databaseManagerForFileAtPath:self.path];
[self.dbm open];
[self getAllTables];
_path = path.copy;
_dbm = [self databaseManagerForFileAtPath:path];
}

return self;
}

- (void)viewDidLoad {
[super viewDidLoad];

self.showsSearchBar = YES;
[self getAllTables];

self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemCompose
target:self
action:@selector(queryButtonPressed)
];
// Cannot run custom queries on realm databases
self.navigationItem.rightBarButtonItem.enabled = [self.dbm
respondsToSelector:@selector(executeStatement:)
];
}

- (void)queryButtonPressed {
FLEXSQLiteDatabaseManager *database = self.dbm;

[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Execute an SQL query");
make.textField(nil);
make.button(@"Run").handler(^(NSArray<NSString *> *strings) {
FLEXSQLResult *result = [database executeStatement:strings[0]];

if (result.message) {
[FLEXAlert showAlert:@"Message" message:result.message from:self];
} else {
UIViewController *resultsScreen = [FLEXTableContentViewController
columns:result.columns rows:result.rows
];

[self.navigationController pushViewController:resultsScreen animated:YES];
}
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}

- (id<FLEXDatabaseManager>)databaseManagerForFileAtPath:(NSString *)path {
NSString *pathExtension = path.pathExtension.lowercaseString;

@@ -55,14 +95,9 @@ - (instancetype)initWithPath:(NSString *)path {
}

- (void)getAllTables {
self.tables = self.filteredTables = [self.dbm queryAllTables];;
self.tables = self.filteredTables = [self.dbm queryAllTables];
}

- (void)viewDidLoad {
[super viewDidLoad];

self.showsSearchBar = YES;
}

#pragma mark - Search bar

@@ -95,15 +130,13 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N
return cell;
}


- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
FLEXTableContentViewController *contentViewController = [FLEXTableContentViewController new];

contentViewController.rows = [self.dbm queryAllDataWithTableName:self.filteredTables[indexPath.row]];
contentViewController.columns = [self.dbm queryAllColumnsWithTableName:self.filteredTables[indexPath.row]];
NSArray *rows = [self.dbm queryAllDataInTable:self.filteredTables[indexPath.row]];
NSArray *columns = [self.dbm queryAllColumnsOfTable:self.filteredTables[indexPath.row]];

contentViewController.title = self.filteredTables[indexPath.row];
[self.navigationController pushViewController:contentViewController animated:YES];
UIViewController *resultsScreen = [FLEXTableContentViewController columns:columns rows:rows];
resultsScreen.title = self.filteredTables[indexPath.row];
[self.navigationController pushViewController:resultsScreen animated:YES];
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
8 changes: 8 additions & 0 deletions FLEX.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -245,6 +245,8 @@
C38EF26323A2FCD20047A7EC /* FLEXViewControllerShortcuts.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF26123A2FCD20047A7EC /* FLEXViewControllerShortcuts.h */; };
C38F3F31230C958F004E3731 /* FLEXAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = C38F3F2F230C958F004E3731 /* FLEXAlert.h */; };
C38F3F32230C958F004E3731 /* FLEXAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = C38F3F30230C958F004E3731 /* FLEXAlert.m */; };
C397E318240EC98F0091E4EC /* FLEXSQLResult.h in Headers */ = {isa = PBXBuildFile; fileRef = C397E316240EC98F0091E4EC /* FLEXSQLResult.h */; };
C397E319240EC98F0091E4EC /* FLEXSQLResult.m in Sources */ = {isa = PBXBuildFile; fileRef = C397E317240EC98F0091E4EC /* FLEXSQLResult.m */; };
C398624D23AD6C67007E6793 /* TBKeyPathSearchController.h in Headers */ = {isa = PBXBuildFile; fileRef = C398624323AD6C67007E6793 /* TBKeyPathSearchController.h */; };
C398624E23AD6C67007E6793 /* TBKeyPath.m in Sources */ = {isa = PBXBuildFile; fileRef = C398624423AD6C67007E6793 /* TBKeyPath.m */; };
C398624F23AD6C67007E6793 /* TBKeyPathTokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = C398624523AD6C67007E6793 /* TBKeyPathTokenizer.h */; };
@@ -588,6 +590,8 @@
C38EF26123A2FCD20047A7EC /* FLEXViewControllerShortcuts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXViewControllerShortcuts.h; sourceTree = "<group>"; };
C38F3F2F230C958F004E3731 /* FLEXAlert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXAlert.h; sourceTree = "<group>"; };
C38F3F30230C958F004E3731 /* FLEXAlert.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXAlert.m; sourceTree = "<group>"; };
C397E316240EC98F0091E4EC /* FLEXSQLResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXSQLResult.h; sourceTree = "<group>"; };
C397E317240EC98F0091E4EC /* FLEXSQLResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXSQLResult.m; sourceTree = "<group>"; };
C398624323AD6C67007E6793 /* TBKeyPathSearchController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBKeyPathSearchController.h; sourceTree = "<group>"; };
C398624423AD6C67007E6793 /* TBKeyPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBKeyPath.m; sourceTree = "<group>"; };
C398624523AD6C67007E6793 /* TBKeyPathTokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBKeyPathTokenizer.h; sourceTree = "<group>"; };
@@ -961,6 +965,8 @@
779B1EBF1C0C4D7C001F5E49 /* DatabaseBrowser */ = {
isa = PBXGroup;
children = (
C397E316240EC98F0091E4EC /* FLEXSQLResult.h */,
C397E317240EC98F0091E4EC /* FLEXSQLResult.m */,
222C88211C7339DC007CA15F /* FLEXRealmDefines.h */,
779B1EC01C0C4D7C001F5E49 /* FLEXDatabaseManager.h */,
224D49A41C673AB5000EAB86 /* FLEXRealmDatabaseManager.h */,
@@ -1477,6 +1483,7 @@
224D49A81C673AB5000EAB86 /* FLEXRealmDatabaseManager.h in Headers */,
C313853F23F5C1A10046E63C /* FLEXViewControllersViewController.h in Headers */,
C398682623AC359600E9E391 /* FLEXShortcutsFactory+Defaults.h in Headers */,
C397E318240EC98F0091E4EC /* FLEXSQLResult.h in Headers */,
7349FD6A22B93CDF00051810 /* FLEXColor.h in Headers */,
C36FBFD3230F3B98008D95D5 /* FLEXMethod.h in Headers */,
C36FBFD8230F3B98008D95D5 /* FLEXPropertyAttributes.h in Headers */,
@@ -1766,6 +1773,7 @@
3A4C950A1B5B21410088C3F2 /* FLEXFieldEditorView.m in Sources */,
3A4C95061B5B21410088C3F2 /* FLEXArgumentInputViewFactory.m in Sources */,
3A4C95291B5B21410088C3F2 /* FLEXInstancesViewController.m in Sources */,
C397E319240EC98F0091E4EC /* FLEXSQLResult.m in Sources */,
C31D93E923E38E97005517BF /* FLEXBlockDescription.m in Sources */,
71E1C2182307FBB800F5032A /* FLEXKeychainTableViewController.m in Sources */,
94A5151E1C4CA1F10063292F /* FLEXExplorerViewController.m in Sources */,