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