Skip to content

Commit

Permalink
Add ability to run SQL queries
Browse files Browse the repository at this point in the history
  • Loading branch information
Les Melnychuk authored and NSExceptional committed Mar 3, 2020
1 parent a556ece commit 15d7d07
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -8,6 +8,7 @@

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

#if __has_include(<Realm/Realm.h>)
#import <Realm/Realm.h>
Expand Down Expand Up @@ -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) {
Expand Down
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
Expand Up @@ -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

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

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

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

return YES;
}

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

Expand All @@ -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);

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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];
}

Expand All @@ -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;
Expand Down
Loading

0 comments on commit 15d7d07

Please sign in to comment.