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