240 lines
7.0 KiB
Objective-C
240 lines
7.0 KiB
Objective-C
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
|
|
#import <EXSQLite/EXSQLite.h>
|
|
|
|
#import <UMFileSystemInterface/UMFileSystemInterface.h>
|
|
|
|
#import <sqlite3.h>
|
|
|
|
@interface EXSQLite ()
|
|
|
|
@property (nonatomic, copy) NSMutableDictionary *cachedDatabases;
|
|
@property (nonatomic, weak) UMModuleRegistry *moduleRegistry;
|
|
|
|
@end
|
|
|
|
@implementation EXSQLite
|
|
|
|
@synthesize cachedDatabases;
|
|
|
|
- (dispatch_queue_t)methodQueue
|
|
{
|
|
return dispatch_get_main_queue();
|
|
}
|
|
|
|
UM_EXPORT_MODULE(ExponentSQLite);
|
|
|
|
- (void)setModuleRegistry:(UMModuleRegistry *)moduleRegistry
|
|
{
|
|
_moduleRegistry = moduleRegistry;
|
|
cachedDatabases = [NSMutableDictionary dictionary];
|
|
}
|
|
|
|
- (NSString *)pathForDatabaseName:(NSString *)name
|
|
{
|
|
id<UMFileSystemInterface> fileSystem = [_moduleRegistry getModuleImplementingProtocol:@protocol(UMFileSystemInterface)];
|
|
if (!fileSystem) {
|
|
UMLogError(@"No FileSystem module.");
|
|
return nil;
|
|
}
|
|
NSString *directory = [fileSystem.documentDirectory stringByAppendingPathComponent:@"SQLite"];
|
|
[fileSystem ensureDirExistsWithPath:directory];
|
|
return [directory stringByAppendingPathComponent:name];
|
|
}
|
|
|
|
- (NSValue *)openDatabase:(NSString *)dbName
|
|
{
|
|
NSValue *cachedDB = nil;
|
|
NSString *path = [self pathForDatabaseName:dbName];
|
|
if (!path) {
|
|
return nil;
|
|
}
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
|
cachedDB = [cachedDatabases objectForKey:dbName];
|
|
}
|
|
if (cachedDB == nil) {
|
|
[cachedDatabases removeObjectForKey:dbName];
|
|
sqlite3 *db;
|
|
if (sqlite3_open([path UTF8String], &db) != SQLITE_OK) {
|
|
return nil;
|
|
};
|
|
cachedDB = [NSValue valueWithPointer:db];
|
|
[cachedDatabases setObject:cachedDB forKey:dbName];
|
|
}
|
|
return cachedDB;
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(exec,
|
|
exec:(NSString *)dbName
|
|
queries:(NSArray *)sqlQueries
|
|
readOnly:(BOOL)readOnly
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
@synchronized(self) {
|
|
NSValue *databasePointer = [self openDatabase:dbName];
|
|
if (!databasePointer) {
|
|
reject(@"E_SQLITE_OPEN_DATABASE", @"Could not open database.", nil);
|
|
return;
|
|
}
|
|
|
|
sqlite3 *db = [databasePointer pointerValue];
|
|
NSMutableArray *sqlResults = [NSMutableArray arrayWithCapacity:sqlQueries.count];
|
|
for (NSArray *sqlQueryObject in sqlQueries) {
|
|
NSString *sql = [sqlQueryObject objectAtIndex:0];
|
|
NSArray *sqlArgs = [sqlQueryObject objectAtIndex:1];
|
|
[sqlResults addObject:[self executeSql:sql withSqlArgs:sqlArgs withDb:db withReadOnly:readOnly]];
|
|
}
|
|
resolve(sqlResults);
|
|
}
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(close,
|
|
close:(NSString *)dbName
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
@synchronized(self) {
|
|
[cachedDatabases removeObjectForKey:dbName];
|
|
resolve(nil);
|
|
}
|
|
}
|
|
|
|
- (id)getSqlValueForColumnType:(int)columnType withStatement:(sqlite3_stmt*)statement withIndex:(int)i
|
|
{
|
|
switch (columnType) {
|
|
case SQLITE_INTEGER:
|
|
return @(sqlite3_column_int64(statement, i));
|
|
case SQLITE_FLOAT:
|
|
return @(sqlite3_column_double(statement, i));
|
|
case SQLITE_BLOB:
|
|
case SQLITE_TEXT:
|
|
return [[NSString alloc] initWithBytes:(char *)sqlite3_column_text(statement, i)
|
|
length:sqlite3_column_bytes(statement, i)
|
|
encoding:NSUTF8StringEncoding];
|
|
}
|
|
return [NSNull null];
|
|
}
|
|
|
|
- (NSArray *)executeSql:(NSString*)sql withSqlArgs:(NSArray*)sqlArgs
|
|
withDb:(sqlite3*)db withReadOnly:(BOOL)readOnly
|
|
{
|
|
NSString *error = nil;
|
|
sqlite3_stmt *statement;
|
|
NSMutableArray *resultRows = [NSMutableArray arrayWithCapacity:0];
|
|
NSMutableArray *entry;
|
|
long long insertId = 0;
|
|
int rowsAffected = 0;
|
|
int i;
|
|
|
|
// compile the statement, throw an error if necessary
|
|
if (sqlite3_prepare_v2(db, [sql UTF8String], -1, &statement, NULL) != SQLITE_OK) {
|
|
error = [EXSQLite convertSQLiteErrorToString:db];
|
|
return @[error];
|
|
}
|
|
|
|
bool queryIsReadOnly = sqlite3_stmt_readonly(statement);
|
|
if (readOnly && !queryIsReadOnly) {
|
|
error = [NSString stringWithFormat:@"could not prepare %@", sql];
|
|
return @[error];
|
|
}
|
|
|
|
// bind any arguments
|
|
if (sqlArgs != nil) {
|
|
for (i = 0; i < sqlArgs.count; i++) {
|
|
[self bindStatement:statement withArg:[sqlArgs objectAtIndex:i] atIndex:(i + 1)];
|
|
}
|
|
}
|
|
|
|
// iterate through sql results
|
|
int columnCount = 0;
|
|
NSMutableArray *columnNames = [NSMutableArray arrayWithCapacity:0];
|
|
NSString *columnName;
|
|
int columnType;
|
|
BOOL fetchedColumns = NO;
|
|
int result;
|
|
NSObject *columnValue;
|
|
BOOL hasMore = YES;
|
|
while (hasMore) {
|
|
result = sqlite3_step (statement);
|
|
switch (result) {
|
|
case SQLITE_ROW:
|
|
if (!fetchedColumns) {
|
|
// get all column names once at the beginning
|
|
columnCount = sqlite3_column_count(statement);
|
|
|
|
for (i = 0; i < columnCount; i++) {
|
|
columnName = [NSString stringWithFormat:@"%s", sqlite3_column_name(statement, i)];
|
|
[columnNames addObject:columnName];
|
|
}
|
|
fetchedColumns = YES;
|
|
}
|
|
entry = [NSMutableArray arrayWithCapacity:columnCount];
|
|
for (i = 0; i < columnCount; i++) {
|
|
columnType = sqlite3_column_type(statement, i);
|
|
columnValue = [self getSqlValueForColumnType:columnType withStatement:statement withIndex: i];
|
|
[entry addObject:columnValue];
|
|
}
|
|
[resultRows addObject:entry];
|
|
break;
|
|
case SQLITE_DONE:
|
|
hasMore = NO;
|
|
break;
|
|
default:
|
|
error = [EXSQLite convertSQLiteErrorToString:db];
|
|
hasMore = NO;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!queryIsReadOnly) {
|
|
rowsAffected = sqlite3_changes(db);
|
|
if (rowsAffected > 0) {
|
|
insertId = sqlite3_last_insert_rowid(db);
|
|
}
|
|
}
|
|
|
|
sqlite3_finalize(statement);
|
|
|
|
if (error) {
|
|
return @[error];
|
|
}
|
|
return @[[NSNull null], @(insertId), @(rowsAffected), columnNames, resultRows];
|
|
}
|
|
|
|
- (void)bindStatement:(sqlite3_stmt *)statement withArg:(NSObject *)arg atIndex:(int)argIndex
|
|
{
|
|
if ([arg isEqual:[NSNull null]]) {
|
|
sqlite3_bind_null(statement, argIndex);
|
|
} else if ([arg isKindOfClass:[NSNumber class]]) {
|
|
sqlite3_bind_double(statement, argIndex, [((NSNumber *) arg) doubleValue]);
|
|
} else { // NSString
|
|
NSString *stringArg;
|
|
|
|
if ([arg isKindOfClass:[NSString class]]) {
|
|
stringArg = (NSString *)arg;
|
|
} else {
|
|
stringArg = [arg description]; // convert to text
|
|
}
|
|
|
|
NSData *data = [stringArg dataUsingEncoding:NSUTF8StringEncoding];
|
|
sqlite3_bind_text(statement, argIndex, data.bytes, (int)data.length, SQLITE_TRANSIENT);
|
|
}
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
for (NSString *key in cachedDatabases) {
|
|
sqlite3_close([[cachedDatabases objectForKey:key] pointerValue]);
|
|
}
|
|
}
|
|
|
|
+ (NSString *)convertSQLiteErrorToString:(struct sqlite3 *)db
|
|
{
|
|
int code = sqlite3_errcode(db);
|
|
NSString *message = [NSString stringWithUTF8String:sqlite3_errmsg(db)];
|
|
return [NSString stringWithFormat:@"Error code %i: %@", code, message];
|
|
}
|
|
|
|
@end
|