//
//  MLSQLite.m
//  Monal
//
//  Created by Thilo Molitor on 31.07.20.
//  Copyright © 2020 Monal.im. All rights reserved.
//

#import <pthread.h>
#import <sqlite3.h>
#import "MLSQLite.h"
#import "HelperTools.h"

@interface MLSQLite()
{
    NSString* _dbFile;
    sqlite3* _database;
}
@end

static NSMutableDictionary* currentTransactions;

@implementation MLSQLite

+(void) initialize
{
    currentTransactions = [NSMutableDictionary new];
    
    if(sqlite3_config(SQLITE_CONFIG_MULTITHREAD) == SQLITE_OK)
        DDLogInfo(@"sqlite initialize: sqlite3 configured ok");
    else
    {
        DDLogError(@"sqlite initialize: sqlite3 not configured ok");
        @throw [NSException exceptionWithName:@"SQLite3Exception" reason:@"sqlite3_config() failed" userInfo:nil];
    }
    
    sqlite3_initialize();
    DDLogInfo(@"sqlite initialize: using mysql lib version: %s", sqlite3_libversion());
}

//every thread gets its own instance having its own db connection
//this allows for concurrent reads/writes
+(id) sharedInstanceForFile:(NSString*) dbFile
{
    MLAssert(dbFile != nil, @"MLSQLite sharedInstanceForFile:nil: file MUST NOT be nil!");
    @synchronized(self) {
        NSMutableDictionary* threadData = [[NSThread currentThread] threadDictionary];
        if(threadData[@"_sqliteInstancesForThread"] && threadData[@"_sqliteInstancesForThread"][dbFile])
            return threadData[@"_sqliteInstancesForThread"][dbFile];
        MLSQLite* newInstance = [[self alloc] initWithFile:dbFile];
        //init dictionaries if neccessary
        if(!threadData[@"_sqliteInstancesForThread"])
            threadData[@"_sqliteInstancesForThread"] = [NSMutableDictionary new];
        if(!threadData[@"_sqliteTransactionsRunning"])
            threadData[@"_sqliteTransactionsRunning"] = [NSMutableDictionary new];
        if(!threadData[@"_sqliteStartedReadTransaction"])
            threadData[@"_sqliteStartedReadTransaction"] = [NSMutableDictionary new];
        //save thread-local instance
        threadData[@"_sqliteInstancesForThread"][dbFile] = newInstance;
        //init data for nested transactions
        threadData[@"_sqliteTransactionsRunning"][dbFile] = [NSNumber numberWithInt:0];
        threadData[@"_sqliteStartedReadTransaction"][dbFile] = @NO;
        return newInstance;
    }
}

-(id) initWithFile:(NSString*) dbFile
{
    _dbFile = dbFile;
    DDLogVerbose(@"db path %@", _dbFile);
    
    //mark all files to stay unlocked even if device gets locked again
    [HelperTools configureFileProtectionFor:_dbFile];
    [HelperTools configureFileProtectionFor:[NSString stringWithFormat:@"%@-wal", _dbFile]];
    [HelperTools configureFileProtectionFor:[NSString stringWithFormat:@"%@-shm", _dbFile]];
    
    if(sqlite3_open_v2([_dbFile UTF8String], &(self->_database), SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nil) == SQLITE_OK)
        DDLogInfo(@"Database opened: %@", _dbFile);
    else
    {
        //database error message
        DDLogError(@"Error opening database: %@", _dbFile);
        @throw [NSException exceptionWithName:@"SQLite3Exception" reason:@"sqlite3_open_v2() failed" userInfo:nil];
    }
    
    //use this observer because dealloc will not be called in the same thread as the sqlite statements got prepared in
    [[NSNotificationCenter defaultCenter] addObserverForName:NSThreadWillExitNotification object:[NSThread currentThread] queue:nil usingBlock:^(NSNotification* notification __unused) {
        @synchronized(self) {
            NSMutableDictionary* threadData = [[NSThread currentThread] threadDictionary];
            if([threadData[@"_sqliteTransactionsRunning"][self->_dbFile] intValue] > 1)
            {
                DDLogError(@"Transaction leak in NSThreadWillExitNotification: trying to close sqlite3 connection while transaction still open");
                @throw [NSException exceptionWithName:@"SQLite3Exception" reason:@"Transaction leak in NSThreadWillExitNotification: trying to close sqlite3 connection while transaction still open" userInfo:threadData];
            }
            if(self->_database)
            {
                DDLogInfo(@"Closing database in NSThreadWillExitNotification: %@", self->_dbFile);
                sqlite3_close(self->_database);
                self->_database = NULL;
            }
        }
    }];

    //some settings (e.g. truncate is faster than delete)
    //this uses the private api because we have no thread local instance added to the threadData dictionary yet and we don't use a transaction either (and public apis check both)
    //--> we must use the internal api because it does not call testThreadInstanceForQuery: testTransactionsForQuery:
    sqlite3_busy_timeout(self->_database, 2000);        //set the busy time as early as possible to make sure the pragma states don't trigger a retry too often
    while([self executeNonQuery:@"PRAGMA synchronous=NORMAL;" andArguments:@[] withException:NO] != YES)
        DDLogError(@"Database locked, while calling 'PRAGMA synchronous=NORMAL;', retrying...");
    while([self executeNonQuery:@"PRAGMA truncate;" andArguments:@[] withException:NO] != YES)
        DDLogError(@"Database locked, while calling 'PRAGMA truncate;', retrying...");
    while([self executeNonQuery:@"PRAGMA foreign_keys=on;" andArguments:@[] withException:NO] != YES)
        DDLogError(@"Database locked, while calling 'PRAGMA foreign_keys=on;', retrying...");
    //this seems to provide *slightly* better security
    //see https://sqlite.org/pragma.html#pragma_trusted_schema
    while([self executeNonQuery:@"PRAGMA trusted_schema = off;" andArguments:@[] withException:NO] != YES)
        DDLogError(@"Database locked, while calling 'PRAGMA trusted_schema = off;', retrying...");

    return self;
}

-(void) dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    @synchronized(self) {
        NSMutableDictionary* threadData = [[NSThread currentThread] threadDictionary];
        if([threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] > 1)
        {
            DDLogError(@"Transaction leak in dealloc: trying to close sqlite3 connection while transaction still open");
            @throw [NSException exceptionWithName:@"SQLite3Exception" reason:@"Transaction leak in dealloc: trying to close sqlite3 connection while transaction still open" userInfo:threadData];
        }
        if(self->_database)
        {
            DDLogInfo(@"Closing database in dealloc: %@", _dbFile);
            sqlite3_close(self->_database);
            self->_database = NULL;
        }
    }
}

-(NSString*) calcThreadName
{
    __uint64_t tid;
    if(pthread_threadid_np(NULL, &tid) == 0)
        return [[NSString alloc] initWithFormat:@"%llu(%@) --> %@", tid, [NSThread currentThread].name, [NSThread currentThread]];
    else
        return [[NSString alloc] initWithFormat:@"missing threadId (%@) --> %@", [NSThread currentThread].name, [NSThread currentThread]];
}

#pragma mark - private sql api

-(sqlite3_stmt*) prepareQuery:(NSString*) query withArgs:(NSArray*) args
{
    sqlite3_stmt* statement;

    if(sqlite3_prepare_v2(self->_database, [query cStringUsingEncoding:NSUTF8StringEncoding], -1, &statement, NULL) != SQLITE_OK)
    {
        [self throwErrorForQuery:query andArguments:args];
        return NULL;
    }
    
    if((int)args.count != sqlite3_bind_parameter_count(statement))
        @throw [NSException exceptionWithName:@"SQLite3Exception" reason:@"SQL parameter count not equals argument count!" userInfo:@{
            @"query": query,
            @"args": args,
            @"paramCount": @(sqlite3_bind_parameter_count(statement)),
            @"argCount": @(args.count),
        }];
    
    //bind args to statement
    sqlite3_reset(statement);
    [args enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop __unused) {
        if([obj isKindOfClass:[NSNumber class]])
        {
            NSNumber* number = (NSNumber*)obj;
            if(sqlite3_bind_double(statement, (signed)idx+1, [number doubleValue]) != SQLITE_OK)
            {
                DDLogError(@"number bind error: %@", number);
                [self throwErrorForQuery:query andArguments:args];
            }
        }
        else if([obj isKindOfClass:[NSString class]])
        {
            NSString* text = (NSString*)obj;
            if(sqlite3_bind_text(statement, (signed)idx+1, [text cStringUsingEncoding:NSUTF8StringEncoding], -1, SQLITE_TRANSIENT) != SQLITE_OK)
            {
                DDLogError(@"text bind error: %@", text);
                [self throwErrorForQuery:query andArguments:args];
            }
        }
        else if([obj isKindOfClass:[NSData class]])
        {
            NSData* data = (NSData*)obj;
            if(sqlite3_bind_blob(statement, (signed)idx+1, [data bytes], (int)data.length, SQLITE_TRANSIENT) != SQLITE_OK)
            {
                DDLogError(@"blob bind error: %@", data);
                [self throwErrorForQuery:query andArguments:args];
            }
        }
        else if([obj isKindOfClass:[NSNull class]])
        {
            if(sqlite3_bind_null(statement, (signed)idx+1) != SQLITE_OK)
            {
                DDLogError(@"null bind error");
                [self throwErrorForQuery:query andArguments:args];
            }
        }
        else
        {
            DDLogError(@"Binding unsupported parameter in: %@", statement);
            [self throwErrorForQuery:query andArguments:args];
        }
    }];
    
    return statement;
}

-(id) getColumn:(int) column ofStatement:(sqlite3_stmt*) statement
{
    switch(sqlite3_column_type(statement, column))
    {
        //SQLITE_INTEGER, SQLITE_FLOAT, SQLITE_TEXT, SQLITE_BLOB, or SQLITE_NULL
        case(SQLITE_INTEGER):
        {
            NSNumber* returnInt = [NSNumber numberWithInt:sqlite3_column_int(statement, column)];
            return returnInt;
        }
        case(SQLITE_FLOAT):
        {
            NSNumber* returnFloat = [NSNumber numberWithDouble:sqlite3_column_double(statement, column)];
            return returnFloat;
        }
        case(SQLITE_TEXT):
        {
            NSString* returnString = [NSString stringWithUTF8String:(const char* _Nonnull) sqlite3_column_text(statement, column)];
            return returnString;
        }
        case(SQLITE_BLOB):
        {
            const char* bytes = (const char* _Nonnull) sqlite3_column_blob(statement, column);
            int size = sqlite3_column_bytes(statement, column);
            NSData* returnData = [NSData dataWithBytes:bytes length:size];
            return returnData;
        }
        case(SQLITE_NULL):
        {
            return nil;
        }
    }
    return nil;
}

-(void) throwErrorForQuery:(NSString*) query andArguments:(NSArray*) args
{
    int errcode = sqlite3_extended_errcode(self->_database);
    NSString* error = [NSString stringWithUTF8String:sqlite3_errmsg(self->_database)];
    DDLogError(@"SQLite Exception: %d %@ for query '%@' having params %@", errcode, error, query ? query : @"", args ? args : @[]);
    @synchronized(currentTransactions) {
        DDLogError(@"currentTransactions: %@", currentTransactions);
        @throw [NSException exceptionWithName:@"SQLite3Exception" reason:[NSString stringWithFormat:@"%d: %@", errcode, error] userInfo:@{
            @"query": query ? query : [NSNull null],
            @"args": args ? args : [NSNull null],
            @"currentTransactions": currentTransactions,
        }];
    }
}

-(void) testThreadInstanceForQuery:(NSString*) query andArguments:(NSArray*) args
{
    NSMutableDictionary* threadData = [[NSThread currentThread] threadDictionary];
    if(!threadData[@"_sqliteInstancesForThread"] || !threadData[@"_sqliteInstancesForThread"][_dbFile] || self != threadData[@"_sqliteInstancesForThread"][_dbFile])
    {
        DDLogError(@"Shared instance of MLSQLite used in wrong thread for query '%@' having params %@", query ? query : @"", args ? args : @[]);
        @synchronized(currentTransactions) {
            DDLogError(@"currentTransactions: %@", currentTransactions);
            @throw [NSException exceptionWithName:@"SQLite3Exception" reason:@"Shared instance of MLSQLite used in wrong thread!" userInfo:@{
                @"currentTransactions": currentTransactions,
                @"query": query ? query : [NSNull null],
                @"args": args ? args : [NSNull null]
            }];
        }
    }
}

-(void) testTransactionsForQuery:(NSString*) query andArguments:(NSArray*) args
{
    //ignore pragma "queries" in this test --> pragma "queries" are allowed outside of transactions, too
    if([[query uppercaseString] hasPrefix:@"PRAGMA "])
        return;
    NSMutableDictionary* threadData = [[NSThread currentThread] threadDictionary];
    if([threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] == 0)
    {
        DDLogError(@"Tried to run query outside of transaction: '%@' having params %@", query ? query : @"", args ? args : @[]);
        @synchronized(currentTransactions) {
            DDLogError(@"currentTransactions: %@", currentTransactions);
            @throw [NSException exceptionWithName:@"SQLite3Exception" reason:@"Tried to run query outside of transaction!" userInfo:@{
                @"currentTransactions": currentTransactions,
                @"query": query ? query : [NSNull null],
                @"args": args ? args : [NSNull null]
            }];
        }
    }
}

-(void) checkQuery:(NSString*) query
{
    if(!query || [query length] == 0)
        @throw [NSException exceptionWithName:@"SQLite3Exception" reason:@"Empty sql query!" userInfo:nil];
}

-(BOOL) executeNonQuery:(NSString*) query andArguments:(NSArray *) args withException:(BOOL) throwException
{
    [self checkQuery:query];
    
    //NOTE: we are not checking the thread instance here in this private api, but in the public api proxy methods
    
    BOOL toReturn;
    sqlite3_stmt* statement = [self prepareQuery:query withArgs:args];
    if(statement != NULL)
    {
        int step;
        while((step=sqlite3_step(statement)) == SQLITE_ROW) {}     //clear data of all returned rows
        sqlite3_finalize(statement);
        if(step == SQLITE_DONE)
            toReturn = YES;
        else
        {
            DDLogVerbose(@"sqlite3_step(%@): %d (%d) [%s] --> %@",
                query,
                step,
                sqlite3_extended_errcode(self->_database),
                sqlite3_errmsg(self->_database),
                [[NSThread currentThread] threadDictionary]
            );
            if(throwException)
                [self throwErrorForQuery:query andArguments:args];
            toReturn = NO;
        }
    }
    else
    {
        DDLogError(@"nonquery returning NO with out OK %@", query);
        if(throwException)
            [self throwErrorForQuery:query andArguments:args];
        toReturn = NO;
    }
    return toReturn;
}

-(id) internalExecuteScalar:(NSString*) query andArguments:(NSArray*) args
{
    id __block toReturn;
    sqlite3_stmt* statement = [self prepareQuery:query withArgs:args];
    if(statement != NULL)
    {
        int step;
        if((step=sqlite3_step(statement)) == SQLITE_ROW)
        {
            toReturn = [self getColumn:0 ofStatement:statement];
            while((step=sqlite3_step(statement)) == SQLITE_ROW) {}     //clear data of all other rows
        }
        sqlite3_finalize(statement);
        if(step != SQLITE_DONE)
            [self throwErrorForQuery:query andArguments:args];
    }
    else
    {
        //if noting else
        [self throwErrorForQuery:query andArguments:args];
    }
    return toReturn;
}

#pragma mark - public API

-(void) voidWriteTransaction:(monal_void_block_t) operations
{
    [self idWriteTransaction:^(void){
        operations();
        return (NSObject*)nil;      //dummy return value
    }];
}

-(BOOL) boolWriteTransaction:(monal_sqlite_bool_operations_t) operations
{
    return [[self idWriteTransaction:^(void){
        return [NSNumber numberWithBool:operations()];
    }] boolValue];
}

-(id) idWriteTransaction:(monal_sqlite_operations_t) operations
{
    [self beginWriteTransaction];
#if !TARGET_OS_SIMULATOR
    NSDate* startTime = [NSDate date];
#endif
    id retval = operations();
#if !TARGET_OS_SIMULATOR
    NSDate* endTime = [NSDate date];
    if([endTime timeIntervalSinceDate:startTime] > 2.0)
        showErrorOnAlpha(nil, @"Write transaction blocking took %fs (longer than 2.0s): %@", (double)[endTime timeIntervalSinceDate:startTime], [NSThread callStackSymbols]);
#endif
    [self endWriteTransaction];
    return retval;
}

-(void) beginWriteTransaction
{
    [self testThreadInstanceForQuery:@"beginWriteTransaction" andArguments:nil];
    NSMutableDictionary* threadData = [[NSThread currentThread] threadDictionary];
    if([threadData[@"_sqliteStartedReadTransaction"][_dbFile] boolValue])
        @synchronized(currentTransactions) {
            @throw [NSException exceptionWithName:@"SQLite3Exception" reason:@"Tried to start write transaction inside running read transaction!" userInfo:@{
                @"currentTransactions": currentTransactions,
            }];
        }
    threadData[@"_sqliteTransactionsRunning"][_dbFile] = [NSNumber numberWithInt:([threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] + 1)];
    if([threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] > 1)
        return;			//begin only outermost transaction
    BOOL retval;
    do {
        retval = [self executeNonQuery:@"BEGIN IMMEDIATE TRANSACTION;" andArguments:@[] withException:NO];
        if(!retval)
        {
            [NSThread sleepForTimeInterval:0.001f];		//wait one millisecond and retry again
            @synchronized(currentTransactions) {
                DDLogWarn(@"Retrying write transaction start: %@", @{
                    @"newWriteTransactionVia": [NSThread callStackSymbols],
                    @"currentTransactions": currentTransactions,
                });
            }
        }
    } while(!retval);
    NSString* ownThread = [self calcThreadName];
    @synchronized(currentTransactions) {
        currentTransactions[ownThread] = [NSThread callStackSymbols];
    }
}

-(void) endWriteTransaction
{
    [self testThreadInstanceForQuery:@"endWriteTransaction" andArguments:nil];
    NSMutableDictionary* threadData = [[NSThread currentThread] threadDictionary];
		    threadData[@"_sqliteTransactionsRunning"][_dbFile] = [NSNumber numberWithInt:[threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] - 1];
    if([threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] == 0)
    {
        [self executeNonQuery:@"COMMIT;" andArguments:@[] withException:YES];        //commit only outermost transaction
        NSString* ownThread = [self calcThreadName];
        @synchronized(currentTransactions) {
            [currentTransactions removeObjectForKey:ownThread];
        }
    }
}

-(void) voidReadTransaction:(monal_void_block_t) operations
{
    [self idReadTransaction:^(void){
        operations();
        return (NSObject*)nil;      //dummy return value
    }];
}

-(BOOL) boolReadTransaction:(monal_sqlite_bool_operations_t) operations
{
    return [[self idReadTransaction:^(void){
        return [NSNumber numberWithBool:operations()];
    }] boolValue];
}

-(id) idReadTransaction:(monal_sqlite_operations_t) operations
{
    [self beginReadTransaction];
    id retval = operations();
    [self endReadTransaction];
    return retval;
}

-(void) beginReadTransaction
{
    [self testThreadInstanceForQuery:@"beginReadTransaction" andArguments:nil];
    NSMutableDictionary* threadData = [[NSThread currentThread] threadDictionary];
    threadData[@"_sqliteTransactionsRunning"][_dbFile] = [NSNumber numberWithInt:([threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] + 1)];
    if([threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] > 1)
        return;			//begin only outermost transaction
    BOOL retval;
    do {
        retval = [self executeNonQuery:@"BEGIN DEFERRED TRANSACTION;" andArguments:@[] withException:NO];
        if(!retval)
        {
            [NSThread sleepForTimeInterval:0.001f];		//wait one millisecond and retry again
            @synchronized(currentTransactions) {
                DDLogWarn(@"Retrying read transaction start: %@", @{
                    @"newReadTransactionVia": [NSThread callStackSymbols],
                    @"currentTransactions": currentTransactions,
                });
            }
        }
    } while(!retval);
    threadData[@"_sqliteStartedReadTransaction"][_dbFile] = @YES;
    NSString* ownThread = [self calcThreadName];
    @synchronized(currentTransactions) {
        currentTransactions[ownThread] = [NSThread callStackSymbols];
    }
}

-(void) endReadTransaction
{
    [self testThreadInstanceForQuery:@"endReadTransaction" andArguments:nil];
    NSMutableDictionary* threadData = [[NSThread currentThread] threadDictionary];
    threadData[@"_sqliteTransactionsRunning"][_dbFile] = [NSNumber numberWithInt:[threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] - 1];
    if([threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] == 0)
    {
        [self executeNonQuery:@"COMMIT;" andArguments:@[] withException:YES];        //commit only outermost transaction
        threadData[@"_sqliteStartedReadTransaction"][_dbFile] = @NO;
        NSString* ownThread = [self calcThreadName];
        @synchronized(currentTransactions) {
            [currentTransactions removeObjectForKey:ownThread];
        }
    }
}

-(id) executeScalar:(NSString*) query
{
    return [self executeScalar:query andArguments:@[]];
}

-(id) executeScalar:(NSString*) query andArguments:(NSArray*) args
{
    [self checkQuery:query];
    [self testThreadInstanceForQuery:query andArguments:args];
    [self testTransactionsForQuery:query andArguments:args];
    
    return [self internalExecuteScalar:query andArguments:args];
}

-(NSArray*) executeScalarReader:(NSString*) query
{
    return [self executeScalarReader:query andArguments:@[]];
}

-(NSArray*) executeScalarReader:(NSString*) query andArguments:(NSArray*) args
{
    [self checkQuery:query];
    [self testThreadInstanceForQuery:query andArguments:args];
    [self testTransactionsForQuery:query andArguments:args];
    
    NSMutableArray* __block toReturn = [NSMutableArray new];
    sqlite3_stmt* statement = [self prepareQuery:query withArgs:args];
    if(statement != NULL)
    {
        int step;
        while((step=sqlite3_step(statement)) == SQLITE_ROW)
        {
            NSObject* returnData = [self getColumn:0 ofStatement:statement];
            //accessing an unset key in NSDictionary will return nil (nil can not be inserted directly into the dictionary)
            if(returnData)
                [toReturn addObject:returnData];
        }
        sqlite3_finalize(statement);
        if(step != SQLITE_DONE)
            [self throwErrorForQuery:query andArguments:args];
    }
    else
    {
        //if noting else
        [self throwErrorForQuery:query andArguments:args];
    }
    return toReturn;
}

-(NSMutableArray*) executeReader:(NSString*) query
{
    return [self executeReader:query andArguments:@[]];
}

-(NSMutableArray*) executeReader:(NSString*) query andArguments:(NSArray*) args
{
    [self checkQuery:query];
    [self testThreadInstanceForQuery:query andArguments:args];
    [self testTransactionsForQuery:query andArguments:args];

    NSMutableArray* toReturn = [NSMutableArray new];
    sqlite3_stmt* statement = [self prepareQuery:query withArgs:args];
    if(statement != NULL)
    {
        int step;
        while((step=sqlite3_step(statement)) == SQLITE_ROW)
        {
            NSMutableDictionary* row = [NSMutableDictionary new];
            int counter = 0;
            while(counter < sqlite3_column_count(statement))
            {
                NSString* columnName = [NSString stringWithUTF8String:sqlite3_column_name(statement, counter)];
                NSObject* returnData = [self getColumn:counter ofStatement:statement];
                //accessing an unset key in NSDictionary will return nil (nil can not be inserted directly into the dictionary)
                if(returnData)
                    [row setObject:returnData forKey:columnName];
                counter++;
            }
            [toReturn addObject:row];
        }
        sqlite3_finalize(statement);
        if(step != SQLITE_DONE)
            [self throwErrorForQuery:query andArguments:args];
    }
    else
    {
        //if noting else
        DDLogVerbose(@"reader nil with sql not ok: %@", query);
        [self throwErrorForQuery:query andArguments:args];
    }
    return toReturn;
}

-(BOOL) executeNonQuery:(NSString*) query
{
    [self testThreadInstanceForQuery:query andArguments:@[]];
    [self testTransactionsForQuery:query andArguments:@[]];
    return [self executeNonQuery:query andArguments:@[] withException:YES];
}

-(BOOL) executeNonQuery:(NSString*) query andArguments:(NSArray*) args
{
    [self testThreadInstanceForQuery:query andArguments:args];
    [self testTransactionsForQuery:query andArguments:args];
    return [self executeNonQuery:query andArguments:args withException:YES];
}

-(NSNumber*) lastInsertId
{
    [self testThreadInstanceForQuery:@"lastInsertId" andArguments:nil];
    [self testTransactionsForQuery:@"lastInsertId" andArguments:nil];
    return [NSNumber numberWithInt:(int)sqlite3_last_insert_rowid(self->_database)];
}

-(void) enableWAL
{
    NSMutableDictionary* threadData = [[NSThread currentThread] threadDictionary];
    MLAssert([threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] == 0, @"Could not enable wal, inside transaction!", (@{
        @"threadDictionary": threadData
    }));
    NSString* mode = [self internalExecuteScalar:@"PRAGMA journal_mode;" andArguments:@[]];
    if([mode isEqualToString:@"wal"])
        return;
    mode = [self internalExecuteScalar:@"PRAGMA journal_mode=WAL;" andArguments:@[]];
    if([mode isEqualToString:@"wal"])
        DDLogWarn(@"Transaction mode set to WAL");
    else
        @throw [NSException exceptionWithName:@"SQLite3Exception" reason:@"Failed to enable sqlite WAL mode" userInfo:@{
            @"file": _dbFile,
            @"mode": mode
        }];
}

-(void) checkpointWal
{
    NSMutableDictionary* threadData = [[NSThread currentThread] threadDictionary];
    //being inside a transaction is non-fatal, the db file will just not be up to date then
    if([threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] == 0)
    {
        NSArray* result = [self executeReader:@"PRAGMA wal_checkpoint(TRUNCATE);"];
        DDLogInfo(@"Chekpointing returned: %@", result);
    }
    else
        DDLogError(@"Could not checkpoint wal, inside transaction: %@", threadData);
}

// optimize db
-(void) vacuum
{
    //trying to vaccum the db inside a transaction is non-fatal, the db file will just not be shrinked then
    DDLogDebug(@"Vacuum DB");
    NSMutableDictionary* threadData = [[NSThread currentThread] threadDictionary];
    if([threadData[@"_sqliteTransactionsRunning"][_dbFile] intValue] == 0)
    {
        [self executeNonQuery:@"VACUUM;" andArguments:@[] withException:YES];
        DDLogDebug(@"Vacuum DB success");
    }
    else
        DDLogError(@"Could not vaccum db, inside transaction: %@", threadData);
}

@end