Files
main/Tweaks/FLEX/GlobalStateExplorers/DatabaseBrowser/FLEXSQLiteDatabaseManager.m
2023-06-27 09:54:41 +02:00

330 lines
11 KiB
Objective-C

//
// PTDatabaseManager.m
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import "FLEXSQLiteDatabaseManager.h"
#import "FLEXManager.h"
#import "NSArray+FLEX.h"
#import "FLEXRuntimeConstants.h"
#import <sqlite3.h>
#define kQuery(name, str) static NSString * const QUERY_##name = str
kQuery(TABLENAMES, @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC");
@interface FLEXSQLiteDatabaseManager ()
@property (nonatomic) sqlite3 *db;
@property (nonatomic, copy) NSString *path;
@end
@implementation FLEXSQLiteDatabaseManager
#pragma mark - FLEXDatabaseManager
+ (instancetype)managerForDatabase:(NSString *)path {
return [[self alloc] initWithPath:path];
}
- (instancetype)initWithPath:(NSString *)path {
self = [super init];
if (self) {
self.path = path;
}
return self;
}
- (void)dealloc {
[self close];
}
- (BOOL)open {
if (self.db) {
return YES;
}
int err = sqlite3_open(self.path.UTF8String, &_db);
#if SQLITE_HAS_CODEC
NSString *defaultSqliteDatabasePassword = FLEXManager.sharedManager.defaultSqliteDatabasePassword;
if (defaultSqliteDatabasePassword) {
const char *key = defaultSqliteDatabasePassword.UTF8String;
sqlite3_key(_db, key, (int)strlen(key));
}
#endif
if (err != SQLITE_OK) {
return [self storeErrorForLastTask:@"Open"];
}
return YES;
}
- (BOOL)close {
if (!self.db) {
return YES;
}
int rc;
BOOL retry, triedFinalizingOpenStatements = NO;
do {
retry = NO;
rc = sqlite3_close(_db);
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
if (!triedFinalizingOpenStatements) {
triedFinalizingOpenStatements = YES;
sqlite3_stmt *pStmt;
while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) {
NSLog(@"Closing leaked statement");
sqlite3_finalize(pStmt);
retry = YES;
}
}
} else if (SQLITE_OK != rc) {
[self storeErrorForLastTask:@"Close"];
self.db = nil;
return NO;
}
} while (retry);
self.db = nil;
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;
}] ?: @[];
}
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
FLEXSQLResult *results = [self executeStatement:sql];
// https://github.com/FLEXTool/FLEX/issues/554
if (!results.keyedRows.count) {
sql = [NSString stringWithFormat:@"SELECT * FROM pragma_table_info('%@')", tableName];
results = [self executeStatement:sql];
// Fallback to empty query
if (!results.keyedRows.count) {
sql = [NSString stringWithFormat:@"SELECT * FROM \"%@\" where 0=1", tableName];
return [self executeStatement:sql].columns ?: @[];
}
}
return [results.keyedRows flex_mapped:^id(NSDictionary *column, NSUInteger idx) {
return column[@"name"];
}] ?: @[];
}
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
NSString *command = [NSString stringWithFormat:@"SELECT * FROM \"%@\"", tableName];
return [self executeStatement:command].rows ?: @[];
}
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName {
NSString *command = [NSString stringWithFormat:QUERY_ROWIDS, tableName];
NSArray<NSArray<NSString *> *> *data = [self executeStatement:command].rows ?: @[];
return [data flex_mapped:^id(NSArray<NSString *> *obj, NSUInteger idx) {
return obj.firstObject;
}];
}
- (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;
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 (columnCount will be 0 for insert/update/delete)
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
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];
}]];
}
}
if (status == SQLITE_DONE) {
// columnCount will be 0 for insert/update/delete
if (rows.count || columnCount > 0) {
// We executed a SELECT query
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 = _lastResult = [FLEXSQLResult message:message];
}
} else {
// An error occured executing the query
result = _lastResult = [self errorResult:@"Execution"];
}
} else {
// An error occurred creating the prepared statement
result = _lastResult = [self errorResult:@"Prepared statement"];
}
sqlite3_finalize(pstmt);
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);
switch (columnType) {
case SQLITE_INTEGER:
return @(sqlite3_column_int64(stmt, columnIdx)).stringValue;
case SQLITE_FLOAT:
return @(sqlite3_column_double(stmt, columnIdx)).stringValue;
case SQLITE_BLOB:
return [NSString stringWithFormat:@"Data (%@ bytes)",
@([self dataForColumnIndex:columnIdx stmt:stmt].length)
];
default:
// Default to a string for everything else
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;
}
const char *text = (const char *)sqlite3_column_text(stmt, columnIdx);
return text ? @(text) : nil;
}
- (NSData *)dataForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt {
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const void *blob = sqlite3_column_blob(stmt, columnIdx);
NSInteger size = (NSInteger)sqlite3_column_bytes(stmt, columnIdx);
return blob ? [NSData dataWithBytes:blob length:size] : nil;
}
@end