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