1c1d37355SAlan Hughes import ExpoModulesCore 274e0b8dfSAlan Hughes import sqlite3 3c1d37355SAlan Hughes 4c1d37355SAlan Hughes public final class SQLiteModule: Module { 5c1d37355SAlan Hughes private var cachedDatabases = [String: OpaquePointer]() 674e0b8dfSAlan Hughes private var hasListeners = false 774e0b8dfSAlan Hughes private lazy var selfPointer = Unmanaged.passRetained(self).toOpaque() 8c1d37355SAlan Hughes definitionnull9c1d37355SAlan Hughes public func definition() -> ModuleDefinition { 10c1d37355SAlan Hughes Name("ExpoSQLite") 11c1d37355SAlan Hughes 1274e0b8dfSAlan Hughes Events("onDatabaseChange") 1374e0b8dfSAlan Hughes 1474e0b8dfSAlan Hughes OnCreate { 1574e0b8dfSAlan Hughes crsqlite_init_from_swift() 1674e0b8dfSAlan Hughes } 1774e0b8dfSAlan Hughes 18c1d37355SAlan Hughes AsyncFunction("exec") { (dbName: String, queries: [[Any]], readOnly: Bool) -> [Any?] in 19c1d37355SAlan Hughes guard let db = openDatabase(dbName: dbName) else { 20c1d37355SAlan Hughes throw DatabaseException() 21c1d37355SAlan Hughes } 22c1d37355SAlan Hughes 23c1d37355SAlan Hughes let results = try queries.map { query in 24c1d37355SAlan Hughes guard let sql = query[0] as? String else { 25c1d37355SAlan Hughes throw InvalidSqlException() 26c1d37355SAlan Hughes } 27c1d37355SAlan Hughes 28c1d37355SAlan Hughes guard let args = query[1] as? [Any] else { 29c1d37355SAlan Hughes throw InvalidArgumentsException() 30c1d37355SAlan Hughes } 31c1d37355SAlan Hughes 32c1d37355SAlan Hughes return executeSql(sql: sql, with: args, for: db, readOnly: readOnly) 33c1d37355SAlan Hughes } 34c1d37355SAlan Hughes 35c1d37355SAlan Hughes return results 36c1d37355SAlan Hughes } 37c1d37355SAlan Hughes 38c1d37355SAlan Hughes AsyncFunction("close") { (dbName: String) in 39c1d37355SAlan Hughes cachedDatabases.removeValue(forKey: dbName) 40c1d37355SAlan Hughes } 41c1d37355SAlan Hughes 426c4baee8SAlan Hughes Function("closeSync") { (dbName: String) in 436c4baee8SAlan Hughes cachedDatabases.removeValue(forKey: dbName) 446c4baee8SAlan Hughes } 456c4baee8SAlan Hughes 46c1d37355SAlan Hughes AsyncFunction("deleteAsync") { (dbName: String) in 47c1d37355SAlan Hughes if cachedDatabases[dbName] != nil { 48c1d37355SAlan Hughes throw DeleteDatabaseException(dbName) 49c1d37355SAlan Hughes } 50c1d37355SAlan Hughes 51c1d37355SAlan Hughes guard let path = self.pathForDatabaseName(name: dbName) else { 52c1d37355SAlan Hughes throw Exceptions.FileSystemModuleNotFound() 53c1d37355SAlan Hughes } 54c1d37355SAlan Hughes 55c1d37355SAlan Hughes if !FileManager.default.fileExists(atPath: path.absoluteString) { 56c1d37355SAlan Hughes throw DatabaseNotFoundException(dbName) 57c1d37355SAlan Hughes } 58c1d37355SAlan Hughes 59c1d37355SAlan Hughes do { 60c1d37355SAlan Hughes try FileManager.default.removeItem(atPath: path.absoluteString) 61c1d37355SAlan Hughes } catch { 62c1d37355SAlan Hughes throw DeleteDatabaseFileException(dbName) 63c1d37355SAlan Hughes } 64c1d37355SAlan Hughes } 65c1d37355SAlan Hughes 6674e0b8dfSAlan Hughes OnStartObserving { 6774e0b8dfSAlan Hughes hasListeners = true 6874e0b8dfSAlan Hughes } 6974e0b8dfSAlan Hughes 7074e0b8dfSAlan Hughes OnStopObserving { 7174e0b8dfSAlan Hughes hasListeners = false 7274e0b8dfSAlan Hughes } 7374e0b8dfSAlan Hughes 74c1d37355SAlan Hughes OnDestroy { 75c1d37355SAlan Hughes cachedDatabases.values.forEach { 7674e0b8dfSAlan Hughes executeSql(sql: "SELECT crsql_finalize()", with: [], for: $0, readOnly: false) 77c1d37355SAlan Hughes sqlite3_close($0) 78c1d37355SAlan Hughes } 79c1d37355SAlan Hughes } 80c1d37355SAlan Hughes } 81c1d37355SAlan Hughes pathForDatabaseNamenull82c1d37355SAlan Hughes private func pathForDatabaseName(name: String) -> URL? { 83c1d37355SAlan Hughes guard let fileSystem = appContext?.fileSystem else { 84c1d37355SAlan Hughes return nil 85c1d37355SAlan Hughes } 86c1d37355SAlan Hughes 8774e0b8dfSAlan Hughes let directory = URL(string: fileSystem.documentDirectory)?.appendingPathComponent("SQLite") 88c1d37355SAlan Hughes fileSystem.ensureDirExists(withPath: directory?.absoluteString) 89c1d37355SAlan Hughes 90c1d37355SAlan Hughes return directory?.appendingPathComponent(name) 91c1d37355SAlan Hughes } 92c1d37355SAlan Hughes openDatabasenull93c1d37355SAlan Hughes private func openDatabase(dbName: String) -> OpaquePointer? { 94c1d37355SAlan Hughes var db: OpaquePointer? 9574e0b8dfSAlan Hughes guard let path = pathForDatabaseName(name: dbName) else { 96c1d37355SAlan Hughes return nil 97c1d37355SAlan Hughes } 98c1d37355SAlan Hughes 99c1d37355SAlan Hughes let fileExists = FileManager.default.fileExists(atPath: path.absoluteString) 100c1d37355SAlan Hughes 101c1d37355SAlan Hughes if fileExists { 102c1d37355SAlan Hughes db = cachedDatabases[dbName] 103c1d37355SAlan Hughes } 104c1d37355SAlan Hughes 105*3273f84bSAlan Hughes if let db { 106*3273f84bSAlan Hughes return db 107*3273f84bSAlan Hughes } 108*3273f84bSAlan Hughes 109c1d37355SAlan Hughes cachedDatabases.removeValue(forKey: dbName) 11074e0b8dfSAlan Hughes 111c1d37355SAlan Hughes if sqlite3_open(path.absoluteString, &db) != SQLITE_OK { 112c1d37355SAlan Hughes return nil 113c1d37355SAlan Hughes } 114c1d37355SAlan Hughes 11574e0b8dfSAlan Hughes sqlite3_update_hook( 116*3273f84bSAlan Hughes db, { (obj, action, _, tableName, rowId) in 11774e0b8dfSAlan Hughes if let obj, let tableName { 11874e0b8dfSAlan Hughes let selfObj = Unmanaged<SQLiteModule>.fromOpaque(obj).takeUnretainedValue() 119*3273f84bSAlan Hughes if selfObj.hasListeners { 12074e0b8dfSAlan Hughes selfObj.sendEvent("onDatabaseChange", [ 12174e0b8dfSAlan Hughes "tableName": String(cString: UnsafePointer(tableName)), 12274e0b8dfSAlan Hughes "rowId": rowId, 12374e0b8dfSAlan Hughes "typeId": SqlAction.fromCode(value: action) 12474e0b8dfSAlan Hughes ]) 12574e0b8dfSAlan Hughes } 126*3273f84bSAlan Hughes } 12774e0b8dfSAlan Hughes }, 12874e0b8dfSAlan Hughes selfPointer 12974e0b8dfSAlan Hughes ) 13074e0b8dfSAlan Hughes 131c1d37355SAlan Hughes cachedDatabases[dbName] = db 132c1d37355SAlan Hughes return db 133c1d37355SAlan Hughes } 134c1d37355SAlan Hughes executeSqlnull135c1d37355SAlan Hughes private func executeSql(sql: String, with args: [Any], for db: OpaquePointer, readOnly: Bool) -> [Any?] { 136c1d37355SAlan Hughes var resultRows = [Any]() 137c1d37355SAlan Hughes var statement: OpaquePointer? 138c1d37355SAlan Hughes var rowsAffected: Int32 = 0 139c1d37355SAlan Hughes var insertId: Int64 = 0 140c1d37355SAlan Hughes var error: String? 141c1d37355SAlan Hughes 142c1d37355SAlan Hughes if sqlite3_prepare_v2(db, sql, -1, &statement, nil) != SQLITE_OK { 143c1d37355SAlan Hughes return [convertSqlLiteErrorToString(db: db)] 144c1d37355SAlan Hughes } 145c1d37355SAlan Hughes 146c1d37355SAlan Hughes let queryIsReadOnly = sqlite3_stmt_readonly(statement) > 0 147c1d37355SAlan Hughes 148c1d37355SAlan Hughes if readOnly && !queryIsReadOnly { 149c1d37355SAlan Hughes return ["could not prepare \(sql)"] 150c1d37355SAlan Hughes } 151c1d37355SAlan Hughes 152c1d37355SAlan Hughes for (index, arg) in args.enumerated() { 153c1d37355SAlan Hughes guard let obj = arg as? NSObject else { continue } 154c1d37355SAlan Hughes bindStatement(statement: statement, with: obj, at: Int32(index + 1)) 155c1d37355SAlan Hughes } 156c1d37355SAlan Hughes 157c1d37355SAlan Hughes var columnCount: Int32 = 0 158c1d37355SAlan Hughes var columnNames = [String]() 159c1d37355SAlan Hughes var columnType: Int32 160c1d37355SAlan Hughes var fetchedColumns = false 161c1d37355SAlan Hughes var value: Any? 162c1d37355SAlan Hughes var hasMore = true 163c1d37355SAlan Hughes 164c1d37355SAlan Hughes while hasMore { 165c1d37355SAlan Hughes let result = sqlite3_step(statement) 166c1d37355SAlan Hughes 167c1d37355SAlan Hughes switch result { 168c1d37355SAlan Hughes case SQLITE_ROW: 169c1d37355SAlan Hughes if !fetchedColumns { 170c1d37355SAlan Hughes columnCount = sqlite3_column_count(statement) 171c1d37355SAlan Hughes 172c1d37355SAlan Hughes for i in 0..<Int(columnCount) { 173c1d37355SAlan Hughes let columnName = NSString(format: "%s", sqlite3_column_name(statement, Int32(i))) as String 174c1d37355SAlan Hughes columnNames.append(columnName) 175c1d37355SAlan Hughes } 176c1d37355SAlan Hughes fetchedColumns = true 177c1d37355SAlan Hughes } 178c1d37355SAlan Hughes 179c1d37355SAlan Hughes var entry = [Any]() 180c1d37355SAlan Hughes 181c1d37355SAlan Hughes for i in 0..<Int(columnCount) { 182c1d37355SAlan Hughes columnType = sqlite3_column_type(statement, Int32(i)) 183c1d37355SAlan Hughes value = getSqlValue(for: columnType, with: statement, index: Int32(i)) 184c1d37355SAlan Hughes entry.append(value) 185c1d37355SAlan Hughes } 186c1d37355SAlan Hughes 187c1d37355SAlan Hughes resultRows.append(entry) 188c1d37355SAlan Hughes case SQLITE_DONE: 189c1d37355SAlan Hughes hasMore = false 190c1d37355SAlan Hughes default: 191c1d37355SAlan Hughes error = convertSqlLiteErrorToString(db: db) 192c1d37355SAlan Hughes hasMore = false 193c1d37355SAlan Hughes } 194c1d37355SAlan Hughes } 195c1d37355SAlan Hughes 196c1d37355SAlan Hughes if !queryIsReadOnly { 197c1d37355SAlan Hughes rowsAffected = sqlite3_changes(db) 198c1d37355SAlan Hughes if rowsAffected > 0 { 199c1d37355SAlan Hughes insertId = sqlite3_last_insert_rowid(db) 200c1d37355SAlan Hughes } 201c1d37355SAlan Hughes } 202c1d37355SAlan Hughes 203c1d37355SAlan Hughes sqlite3_finalize(statement) 204c1d37355SAlan Hughes 205c1d37355SAlan Hughes if error != nil { 206c1d37355SAlan Hughes return [error] 207c1d37355SAlan Hughes } 208c1d37355SAlan Hughes 209c1d37355SAlan Hughes return [nil, insertId, rowsAffected, columnNames, resultRows] 210c1d37355SAlan Hughes } 211c1d37355SAlan Hughes bindStatementnull212c1d37355SAlan Hughes private func bindStatement(statement: OpaquePointer?, with arg: NSObject, at index: Int32) { 213c1d37355SAlan Hughes if arg == NSNull() { 214c1d37355SAlan Hughes sqlite3_bind_null(statement, index) 215c1d37355SAlan Hughes } else if arg is Double { 216c1d37355SAlan Hughes sqlite3_bind_double(statement, index, arg as? Double ?? 0.0) 217c1d37355SAlan Hughes } else { 218c1d37355SAlan Hughes var stringArg: NSString 219c1d37355SAlan Hughes 220c1d37355SAlan Hughes if arg is NSString { 221c1d37355SAlan Hughes stringArg = NSString(format: "%@", arg) 222c1d37355SAlan Hughes } else { 223c1d37355SAlan Hughes stringArg = arg.description as NSString 224c1d37355SAlan Hughes } 225c1d37355SAlan Hughes 226c1d37355SAlan Hughes let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_destructor_type.self) 227c1d37355SAlan Hughes 228c1d37355SAlan Hughes let data = stringArg.data(using: NSUTF8StringEncoding) 229c1d37355SAlan Hughes sqlite3_bind_text(statement, index, stringArg.utf8String, Int32(data?.count ?? 0), SQLITE_TRANSIENT) 230c1d37355SAlan Hughes } 231c1d37355SAlan Hughes } 232c1d37355SAlan Hughes getSqlValuenull233c1d37355SAlan Hughes private func getSqlValue(for columnType: Int32, with statement: OpaquePointer?, index: Int32) -> Any? { 234c1d37355SAlan Hughes switch columnType { 235c1d37355SAlan Hughes case SQLITE_INTEGER: 236c1d37355SAlan Hughes return sqlite3_column_int64(statement, index) 237c1d37355SAlan Hughes case SQLITE_FLOAT: 238c1d37355SAlan Hughes return sqlite3_column_double(statement, index) 239c1d37355SAlan Hughes case SQLITE_BLOB, SQLITE_TEXT: 240c1d37355SAlan Hughes return NSString(bytes: sqlite3_column_text(statement, index), length: Int(sqlite3_column_bytes(statement, index)), encoding: NSUTF8StringEncoding) 241c1d37355SAlan Hughes default: 242c1d37355SAlan Hughes return nil 243c1d37355SAlan Hughes } 244c1d37355SAlan Hughes } 245c1d37355SAlan Hughes convertSqlLiteErrorToStringnull246c1d37355SAlan Hughes private func convertSqlLiteErrorToString(db: OpaquePointer?) -> String { 247c1d37355SAlan Hughes let code = sqlite3_errcode(db) 248c1d37355SAlan Hughes let message = NSString(utf8String: sqlite3_errmsg(db)) ?? "" 249c1d37355SAlan Hughes return NSString(format: "Error code %i: %@", code, message) as String 250c1d37355SAlan Hughes } 251c1d37355SAlan Hughes } 25274e0b8dfSAlan Hughes 253*3273f84bSAlan Hughes enum SqlAction: String, Enumerable { 25474e0b8dfSAlan Hughes case insert 25574e0b8dfSAlan Hughes case delete 25674e0b8dfSAlan Hughes case update 25774e0b8dfSAlan Hughes case unknown 25874e0b8dfSAlan Hughes fromCodenull25974e0b8dfSAlan Hughes static func fromCode(value: Int32) -> SqlAction { 26074e0b8dfSAlan Hughes switch value { 26174e0b8dfSAlan Hughes case 9: 26274e0b8dfSAlan Hughes return .delete 26374e0b8dfSAlan Hughes case 18: 26474e0b8dfSAlan Hughes return .insert 26574e0b8dfSAlan Hughes case 23: 26674e0b8dfSAlan Hughes return .update 26774e0b8dfSAlan Hughes default: 26874e0b8dfSAlan Hughes return .unknown 26974e0b8dfSAlan Hughes } 27074e0b8dfSAlan Hughes } 27174e0b8dfSAlan Hughes } 272