import ExpoModulesCore import sqlite3 public final class SQLiteModule: Module { private var cachedDatabases = [String: OpaquePointer]() private var hasListeners = false private lazy var selfPointer = Unmanaged.passRetained(self).toOpaque() public func definition() -> ModuleDefinition { Name("ExpoSQLite") Events("onDatabaseChange") OnCreate { crsqlite_init_from_swift() } AsyncFunction("exec") { (dbName: String, queries: [[Any]], readOnly: Bool) -> [Any?] in guard let db = openDatabase(dbName: dbName) else { throw DatabaseException() } let results = try queries.map { query in guard let sql = query[0] as? String else { throw InvalidSqlException() } guard let args = query[1] as? [Any] else { throw InvalidArgumentsException() } return executeSql(sql: sql, with: args, for: db, readOnly: readOnly) } return results } AsyncFunction("close") { (dbName: String) in cachedDatabases.removeValue(forKey: dbName) } Function("closeSync") { (dbName: String) in cachedDatabases.removeValue(forKey: dbName) } AsyncFunction("deleteAsync") { (dbName: String) in if cachedDatabases[dbName] != nil { throw DeleteDatabaseException(dbName) } guard let path = self.pathForDatabaseName(name: dbName) else { throw Exceptions.FileSystemModuleNotFound() } if !FileManager.default.fileExists(atPath: path.absoluteString) { throw DatabaseNotFoundException(dbName) } do { try FileManager.default.removeItem(atPath: path.absoluteString) } catch { throw DeleteDatabaseFileException(dbName) } } OnStartObserving { hasListeners = true } OnStopObserving { hasListeners = false } OnDestroy { cachedDatabases.values.forEach { executeSql(sql: "SELECT crsql_finalize()", with: [], for: $0, readOnly: false) sqlite3_close($0) } } } private func pathForDatabaseName(name: String) -> URL? { guard let fileSystem = appContext?.fileSystem else { return nil } let directory = URL(string: fileSystem.documentDirectory)?.appendingPathComponent("SQLite") fileSystem.ensureDirExists(withPath: directory?.absoluteString) return directory?.appendingPathComponent(name) } private func openDatabase(dbName: String) -> OpaquePointer? { var db: OpaquePointer? guard let path = pathForDatabaseName(name: dbName) else { return nil } let fileExists = FileManager.default.fileExists(atPath: path.absoluteString) if fileExists { db = cachedDatabases[dbName] } if let db { return db } cachedDatabases.removeValue(forKey: dbName) if sqlite3_open(path.absoluteString, &db) != SQLITE_OK { return nil } sqlite3_update_hook( db, { (obj, action, _, tableName, rowId) in if let obj, let tableName { let selfObj = Unmanaged.fromOpaque(obj).takeUnretainedValue() if selfObj.hasListeners { selfObj.sendEvent("onDatabaseChange", [ "tableName": String(cString: UnsafePointer(tableName)), "rowId": rowId, "typeId": SqlAction.fromCode(value: action) ]) } } }, selfPointer ) cachedDatabases[dbName] = db return db } private func executeSql(sql: String, with args: [Any], for db: OpaquePointer, readOnly: Bool) -> [Any?] { var resultRows = [Any]() var statement: OpaquePointer? var rowsAffected: Int32 = 0 var insertId: Int64 = 0 var error: String? if sqlite3_prepare_v2(db, sql, -1, &statement, nil) != SQLITE_OK { return [convertSqlLiteErrorToString(db: db)] } let queryIsReadOnly = sqlite3_stmt_readonly(statement) > 0 if readOnly && !queryIsReadOnly { return ["could not prepare \(sql)"] } for (index, arg) in args.enumerated() { guard let obj = arg as? NSObject else { continue } bindStatement(statement: statement, with: obj, at: Int32(index + 1)) } var columnCount: Int32 = 0 var columnNames = [String]() var columnType: Int32 var fetchedColumns = false var value: Any? var hasMore = true while hasMore { let result = sqlite3_step(statement) switch result { case SQLITE_ROW: if !fetchedColumns { columnCount = sqlite3_column_count(statement) for i in 0.. 0 { insertId = sqlite3_last_insert_rowid(db) } } sqlite3_finalize(statement) if error != nil { return [error] } return [nil, insertId, rowsAffected, columnNames, resultRows] } private func bindStatement(statement: OpaquePointer?, with arg: NSObject, at index: Int32) { if arg == NSNull() { sqlite3_bind_null(statement, index) } else if arg is Double { sqlite3_bind_double(statement, index, arg as? Double ?? 0.0) } else { var stringArg: NSString if arg is NSString { stringArg = NSString(format: "%@", arg) } else { stringArg = arg.description as NSString } let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_destructor_type.self) let data = stringArg.data(using: NSUTF8StringEncoding) sqlite3_bind_text(statement, index, stringArg.utf8String, Int32(data?.count ?? 0), SQLITE_TRANSIENT) } } private func getSqlValue(for columnType: Int32, with statement: OpaquePointer?, index: Int32) -> Any? { switch columnType { case SQLITE_INTEGER: return sqlite3_column_int64(statement, index) case SQLITE_FLOAT: return sqlite3_column_double(statement, index) case SQLITE_BLOB, SQLITE_TEXT: return NSString(bytes: sqlite3_column_text(statement, index), length: Int(sqlite3_column_bytes(statement, index)), encoding: NSUTF8StringEncoding) default: return nil } } private func convertSqlLiteErrorToString(db: OpaquePointer?) -> String { let code = sqlite3_errcode(db) let message = NSString(utf8String: sqlite3_errmsg(db)) ?? "" return NSString(format: "Error code %i: %@", code, message) as String } } enum SqlAction: String, Enumerable { case insert case delete case update case unknown static func fromCode(value: Int32) -> SqlAction { switch value { case 9: return .delete case 18: return .insert case 23: return .update default: return .unknown } } }