1 import ABI49_0_0ExpoModulesCore
2 import SQLite3
3 
4 public final class SQLiteModule: Module {
5   private var cachedDatabases = [String: OpaquePointer]()
6 
definitionnull7   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 
pathForDatabaseNamenull61   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 
openDatabasenull72   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 
executeSqlnull95   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 
bindStatementnull172   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 
getSqlValuenull193   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 
convertSqlLiteErrorToStringnull206   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