1*af2ec015STomasz Sapeta import ABI49_0_0ExpoModulesCore 2*af2ec015STomasz Sapeta import SQLite3 3*af2ec015STomasz Sapeta 4*af2ec015STomasz Sapeta public final class SQLiteModule: Module { 5*af2ec015STomasz Sapeta private var cachedDatabases = [String: OpaquePointer]() 6*af2ec015STomasz Sapeta definitionnull7*af2ec015STomasz Sapeta public func definition() -> ModuleDefinition { 8*af2ec015STomasz Sapeta Name("ExpoSQLite") 9*af2ec015STomasz Sapeta 10*af2ec015STomasz Sapeta AsyncFunction("exec") { (dbName: String, queries: [[Any]], readOnly: Bool) -> [Any?] in 11*af2ec015STomasz Sapeta guard let db = openDatabase(dbName: dbName) else { 12*af2ec015STomasz Sapeta throw DatabaseException() 13*af2ec015STomasz Sapeta } 14*af2ec015STomasz Sapeta 15*af2ec015STomasz Sapeta let results = try queries.map { query in 16*af2ec015STomasz Sapeta guard let sql = query[0] as? String else { 17*af2ec015STomasz Sapeta throw InvalidSqlException() 18*af2ec015STomasz Sapeta } 19*af2ec015STomasz Sapeta 20*af2ec015STomasz Sapeta guard let args = query[1] as? [Any] else { 21*af2ec015STomasz Sapeta throw InvalidArgumentsException() 22*af2ec015STomasz Sapeta } 23*af2ec015STomasz Sapeta 24*af2ec015STomasz Sapeta return executeSql(sql: sql, with: args, for: db, readOnly: readOnly) 25*af2ec015STomasz Sapeta } 26*af2ec015STomasz Sapeta 27*af2ec015STomasz Sapeta return results 28*af2ec015STomasz Sapeta } 29*af2ec015STomasz Sapeta 30*af2ec015STomasz Sapeta AsyncFunction("close") { (dbName: String) in 31*af2ec015STomasz Sapeta cachedDatabases.removeValue(forKey: dbName) 32*af2ec015STomasz Sapeta } 33*af2ec015STomasz Sapeta 34*af2ec015STomasz Sapeta AsyncFunction("deleteAsync") { (dbName: String) in 35*af2ec015STomasz Sapeta if cachedDatabases[dbName] != nil { 36*af2ec015STomasz Sapeta throw DeleteDatabaseException(dbName) 37*af2ec015STomasz Sapeta } 38*af2ec015STomasz Sapeta 39*af2ec015STomasz Sapeta guard let path = self.pathForDatabaseName(name: dbName) else { 40*af2ec015STomasz Sapeta throw Exceptions.FileSystemModuleNotFound() 41*af2ec015STomasz Sapeta } 42*af2ec015STomasz Sapeta 43*af2ec015STomasz Sapeta if !FileManager.default.fileExists(atPath: path.absoluteString) { 44*af2ec015STomasz Sapeta throw DatabaseNotFoundException(dbName) 45*af2ec015STomasz Sapeta } 46*af2ec015STomasz Sapeta 47*af2ec015STomasz Sapeta do { 48*af2ec015STomasz Sapeta try FileManager.default.removeItem(atPath: path.absoluteString) 49*af2ec015STomasz Sapeta } catch { 50*af2ec015STomasz Sapeta throw DeleteDatabaseFileException(dbName) 51*af2ec015STomasz Sapeta } 52*af2ec015STomasz Sapeta } 53*af2ec015STomasz Sapeta 54*af2ec015STomasz Sapeta OnDestroy { 55*af2ec015STomasz Sapeta cachedDatabases.values.forEach { 56*af2ec015STomasz Sapeta sqlite3_close($0) 57*af2ec015STomasz Sapeta } 58*af2ec015STomasz Sapeta } 59*af2ec015STomasz Sapeta } 60*af2ec015STomasz Sapeta pathForDatabaseNamenull61*af2ec015STomasz Sapeta private func pathForDatabaseName(name: String) -> URL? { 62*af2ec015STomasz Sapeta guard let fileSystem = appContext?.fileSystem else { 63*af2ec015STomasz Sapeta return nil 64*af2ec015STomasz Sapeta } 65*af2ec015STomasz Sapeta 66*af2ec015STomasz Sapeta var directory = URL(string: fileSystem.documentDirectory)?.appendingPathComponent("SQLite") 67*af2ec015STomasz Sapeta fileSystem.ensureDirExists(withPath: directory?.absoluteString) 68*af2ec015STomasz Sapeta 69*af2ec015STomasz Sapeta return directory?.appendingPathComponent(name) 70*af2ec015STomasz Sapeta } 71*af2ec015STomasz Sapeta openDatabasenull72*af2ec015STomasz Sapeta private func openDatabase(dbName: String) -> OpaquePointer? { 73*af2ec015STomasz Sapeta var db: OpaquePointer? 74*af2ec015STomasz Sapeta guard let path = try pathForDatabaseName(name: dbName) else { 75*af2ec015STomasz Sapeta return nil 76*af2ec015STomasz Sapeta } 77*af2ec015STomasz Sapeta 78*af2ec015STomasz Sapeta let fileExists = FileManager.default.fileExists(atPath: path.absoluteString) 79*af2ec015STomasz Sapeta 80*af2ec015STomasz Sapeta if fileExists { 81*af2ec015STomasz Sapeta db = cachedDatabases[dbName] 82*af2ec015STomasz Sapeta } 83*af2ec015STomasz Sapeta 84*af2ec015STomasz Sapeta if db == nil { 85*af2ec015STomasz Sapeta cachedDatabases.removeValue(forKey: dbName) 86*af2ec015STomasz Sapeta if sqlite3_open(path.absoluteString, &db) != SQLITE_OK { 87*af2ec015STomasz Sapeta return nil 88*af2ec015STomasz Sapeta } 89*af2ec015STomasz Sapeta 90*af2ec015STomasz Sapeta cachedDatabases[dbName] = db 91*af2ec015STomasz Sapeta } 92*af2ec015STomasz Sapeta return db 93*af2ec015STomasz Sapeta } 94*af2ec015STomasz Sapeta executeSqlnull95*af2ec015STomasz Sapeta private func executeSql(sql: String, with args: [Any], for db: OpaquePointer, readOnly: Bool) -> [Any?] { 96*af2ec015STomasz Sapeta var resultRows = [Any]() 97*af2ec015STomasz Sapeta var statement: OpaquePointer? 98*af2ec015STomasz Sapeta var rowsAffected: Int32 = 0 99*af2ec015STomasz Sapeta var insertId: Int64 = 0 100*af2ec015STomasz Sapeta var error: String? 101*af2ec015STomasz Sapeta 102*af2ec015STomasz Sapeta if sqlite3_prepare_v2(db, sql, -1, &statement, nil) != SQLITE_OK { 103*af2ec015STomasz Sapeta return [convertSqlLiteErrorToString(db: db)] 104*af2ec015STomasz Sapeta } 105*af2ec015STomasz Sapeta 106*af2ec015STomasz Sapeta let queryIsReadOnly = sqlite3_stmt_readonly(statement) > 0 107*af2ec015STomasz Sapeta 108*af2ec015STomasz Sapeta if readOnly && !queryIsReadOnly { 109*af2ec015STomasz Sapeta return ["could not prepare \(sql)"] 110*af2ec015STomasz Sapeta } 111*af2ec015STomasz Sapeta 112*af2ec015STomasz Sapeta for (index, arg) in args.enumerated() { 113*af2ec015STomasz Sapeta guard let obj = arg as? NSObject else { continue } 114*af2ec015STomasz Sapeta bindStatement(statement: statement, with: obj, at: Int32(index + 1)) 115*af2ec015STomasz Sapeta } 116*af2ec015STomasz Sapeta 117*af2ec015STomasz Sapeta var columnCount: Int32 = 0 118*af2ec015STomasz Sapeta var columnNames = [String]() 119*af2ec015STomasz Sapeta var columnType: Int32 120*af2ec015STomasz Sapeta var fetchedColumns = false 121*af2ec015STomasz Sapeta var value: Any? 122*af2ec015STomasz Sapeta var hasMore = true 123*af2ec015STomasz Sapeta 124*af2ec015STomasz Sapeta while hasMore { 125*af2ec015STomasz Sapeta let result = sqlite3_step(statement) 126*af2ec015STomasz Sapeta 127*af2ec015STomasz Sapeta switch result { 128*af2ec015STomasz Sapeta case SQLITE_ROW: 129*af2ec015STomasz Sapeta if !fetchedColumns { 130*af2ec015STomasz Sapeta columnCount = sqlite3_column_count(statement) 131*af2ec015STomasz Sapeta 132*af2ec015STomasz Sapeta for i in 0..<Int(columnCount) { 133*af2ec015STomasz Sapeta let columnName = NSString(format: "%s", sqlite3_column_name(statement, Int32(i))) as String 134*af2ec015STomasz Sapeta columnNames.append(columnName) 135*af2ec015STomasz Sapeta } 136*af2ec015STomasz Sapeta fetchedColumns = true 137*af2ec015STomasz Sapeta } 138*af2ec015STomasz Sapeta 139*af2ec015STomasz Sapeta var entry = [Any]() 140*af2ec015STomasz Sapeta 141*af2ec015STomasz Sapeta for i in 0..<Int(columnCount) { 142*af2ec015STomasz Sapeta columnType = sqlite3_column_type(statement, Int32(i)) 143*af2ec015STomasz Sapeta value = getSqlValue(for: columnType, with: statement, index: Int32(i)) 144*af2ec015STomasz Sapeta entry.append(value) 145*af2ec015STomasz Sapeta } 146*af2ec015STomasz Sapeta 147*af2ec015STomasz Sapeta resultRows.append(entry) 148*af2ec015STomasz Sapeta case SQLITE_DONE: 149*af2ec015STomasz Sapeta hasMore = false 150*af2ec015STomasz Sapeta default: 151*af2ec015STomasz Sapeta error = convertSqlLiteErrorToString(db: db) 152*af2ec015STomasz Sapeta hasMore = false 153*af2ec015STomasz Sapeta } 154*af2ec015STomasz Sapeta } 155*af2ec015STomasz Sapeta 156*af2ec015STomasz Sapeta if !queryIsReadOnly { 157*af2ec015STomasz Sapeta rowsAffected = sqlite3_changes(db) 158*af2ec015STomasz Sapeta if rowsAffected > 0 { 159*af2ec015STomasz Sapeta insertId = sqlite3_last_insert_rowid(db) 160*af2ec015STomasz Sapeta } 161*af2ec015STomasz Sapeta } 162*af2ec015STomasz Sapeta 163*af2ec015STomasz Sapeta sqlite3_finalize(statement) 164*af2ec015STomasz Sapeta 165*af2ec015STomasz Sapeta if error != nil { 166*af2ec015STomasz Sapeta return [error] 167*af2ec015STomasz Sapeta } 168*af2ec015STomasz Sapeta 169*af2ec015STomasz Sapeta return [nil, insertId, rowsAffected, columnNames, resultRows] 170*af2ec015STomasz Sapeta } 171*af2ec015STomasz Sapeta bindStatementnull172*af2ec015STomasz Sapeta private func bindStatement(statement: OpaquePointer?, with arg: NSObject, at index: Int32) { 173*af2ec015STomasz Sapeta if arg == NSNull() { 174*af2ec015STomasz Sapeta sqlite3_bind_null(statement, index) 175*af2ec015STomasz Sapeta } else if arg is Double { 176*af2ec015STomasz Sapeta sqlite3_bind_double(statement, index, arg as? Double ?? 0.0) 177*af2ec015STomasz Sapeta } else { 178*af2ec015STomasz Sapeta var stringArg: NSString 179*af2ec015STomasz Sapeta 180*af2ec015STomasz Sapeta if arg is NSString { 181*af2ec015STomasz Sapeta stringArg = NSString(format: "%@", arg) 182*af2ec015STomasz Sapeta } else { 183*af2ec015STomasz Sapeta stringArg = arg.description as NSString 184*af2ec015STomasz Sapeta } 185*af2ec015STomasz Sapeta 186*af2ec015STomasz Sapeta let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_destructor_type.self) 187*af2ec015STomasz Sapeta 188*af2ec015STomasz Sapeta let data = stringArg.data(using: NSUTF8StringEncoding) 189*af2ec015STomasz Sapeta sqlite3_bind_text(statement, index, stringArg.utf8String, Int32(data?.count ?? 0), SQLITE_TRANSIENT) 190*af2ec015STomasz Sapeta } 191*af2ec015STomasz Sapeta } 192*af2ec015STomasz Sapeta getSqlValuenull193*af2ec015STomasz Sapeta private func getSqlValue(for columnType: Int32, with statement: OpaquePointer?, index: Int32) -> Any? { 194*af2ec015STomasz Sapeta switch columnType { 195*af2ec015STomasz Sapeta case SQLITE_INTEGER: 196*af2ec015STomasz Sapeta return sqlite3_column_int64(statement, index) 197*af2ec015STomasz Sapeta case SQLITE_FLOAT: 198*af2ec015STomasz Sapeta return sqlite3_column_double(statement, index) 199*af2ec015STomasz Sapeta case SQLITE_BLOB, SQLITE_TEXT: 200*af2ec015STomasz Sapeta return NSString(bytes: sqlite3_column_text(statement, index), length: Int(sqlite3_column_bytes(statement, index)), encoding: NSUTF8StringEncoding) 201*af2ec015STomasz Sapeta default: 202*af2ec015STomasz Sapeta return nil 203*af2ec015STomasz Sapeta } 204*af2ec015STomasz Sapeta } 205*af2ec015STomasz Sapeta convertSqlLiteErrorToStringnull206*af2ec015STomasz Sapeta private func convertSqlLiteErrorToString(db: OpaquePointer?) -> String { 207*af2ec015STomasz Sapeta let code = sqlite3_errcode(db) 208*af2ec015STomasz Sapeta let message = NSString(utf8String: sqlite3_errmsg(db)) ?? "" 209*af2ec015STomasz Sapeta return NSString(format: "Error code %i: %@", code, message) as String 210*af2ec015STomasz Sapeta } 211*af2ec015STomasz Sapeta } 212