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