1 import Flutter
2 import EJDB2
3 import PathKit
4
5 typealias MethodInvocation = (_: DbEntry) -> Void
6
7 typealias DbQueryHandler = (_: SWJQL, _: String?) throws -> Void
8
9 extension Dictionary {
containsKeynull10 func containsKey(_ key: Key) -> Bool {
11 self[key] != nil
12 }
13 }
14
asBooleannull15 func asBoolean(_ v: Any?, _ def: Bool = false) -> Bool {
16 if let c = v as? NSNumber {
17 return c.intValue > 0
18 }
19 if let c = v as? NSString {
20 return c.isEqual(to: "true")
21 }
22 if let c = v as? Bool {
23 return c
24 }
25 return def
26 }
27
asNumbernull28 func asNumber(_ v: Any?) -> NSNumber? {
29 if let c = v as? NSNumber {
30 return c
31 }
32 if let c = v as? String {
33 return NSNumber(value: Int64(c) ?? 0)
34 }
35 return nil
36 }
37
asDoublenull38 func asDouble(_ v: Any?) -> Double? {
39 if let c = v as? NSNumber {
40 return c.doubleValue
41 }
42 if let c = v as? String {
43 return Double(c)
44 }
45 return nil
46 }
47
asStringnull48 func asString(_ v: Any?, _ def: String? = nil) -> String? {
49 if let c = v as? String {
50 return c
51 }
52 if v != nil {
53 return String(describing: v!)
54 }
55 return def
56 }
57
58 class DbEntry {
59 let db: EJDB2DB
60 let handle: UInt32
61 let path: String
62 var counter: Int = 0
63
64 init(_ db: EJDB2DB, _ handle: UInt32, _ path: String) {
65 self.db = db
66 self.handle = handle
67 self.path = path
68 }
69
countOpennull70 func countOpen() {
71 counter += 1
72 }
73
closenull74 func close() throws -> Bool {
75 counter -= 1
76 if counter <= 0 {
77 try db.close()
78 }
79 return counter <= 0
80 }
81 }
82
83 struct DbMethodCall {
84 let dbe: DbEntry?
85 let args: [Any?]
86 let result: FlutterResult
87
88 var db: EJDB2DB {
89 dbe!.db
90 }
91
successOnMainThreadnull92 func successOnMainThread(_ val: Any? = nil) {
93 DispatchQueue.main.async {
94 self.result(val)
95 }
96 }
97
errorOnMainThreadnull98 func errorOnMainThread(_ code: String, _ message: String?, _ details: Any? = nil) {
99 DispatchQueue.main.async {
100 self.result(FlutterError(code: code, message: message, details: details))
101 }
102 }
103 }
104
105 public class SwiftEjdb2FlutterPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
106
registernull107 public static func register(with registrar: FlutterPluginRegistrar) {
108 let _ = SwiftEjdb2FlutterPlugin(registrar)
109 }
110
111 let methodChannel: FlutterMethodChannel
112 let eventChannel: FlutterEventChannel
113 var dbmap: [UInt32: DbEntry] = [:]
114 var dbkeys: UInt32 = 0
115 var events: FlutterEventSink?
116
117 init(_ registrar: FlutterPluginRegistrar) {
118 self.methodChannel = FlutterMethodChannel(name: "ejdb2", binaryMessenger: registrar.messenger())
119 self.eventChannel = FlutterEventChannel(name: "ejdb2/query", binaryMessenger: registrar.messenger())
120 super.init()
121 registrar.addMethodCallDelegate(self, channel: methodChannel)
122 eventChannel.setStreamHandler(self)
123 }
124
125 func execute(_ mc: DbMethodCall, _ block: @escaping (_: DbMethodCall) throws -> Void) {
126 DispatchQueue.global().async {
127 do {
128 try block(mc)
129 } catch let error as EJDB2Error {
130 mc.errorOnMainThread("@ejdb IWRC:\(error.code)", error.message)
131 } catch {
132 mc.errorOnMainThread("@ejdb", "\(error)")
133 }
134 }
135 }
136
handlenull137 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
138 // NSLog("\nCall method \(call.method)")
139 let args = call.arguments as? [Any?] ?? []
140 var dbe: DbEntry?
141 if let dbid = args.first as? NSNumber {
142 dbe = dbmap[dbid.uint32Value]
143 if dbe == nil {
144 result(FlutterError(code: "@ejdb", message: "Database handle already disposed", details: nil))
145 return
146 }
147 }
148
149 let mc = DbMethodCall(dbe: dbe, args: args, result: result)
150
151 switch call.method {
152 case "get":
153 execute(mc, get)
154 break
155 case "put":
156 execute(mc, put)
157 break;
158 case "patch":
159 execute(mc, patch)
160 break
161 case "del":
162 execute(mc, del)
163 break
164 case "info":
165 execute(mc, info)
166 break
167 case "executeList":
168 execute(mc, executeList)
169 break
170 case "executeFirst":
171 execute(mc, executeFirst)
172 break
173 case "executeScalarInt":
174 execute(mc, executeScalarInt)
175 break
176 case "executeQuery":
177 execute(mc, executeQuery)
178 break
179 case "renameCollection":
180 execute(mc, renameCollection)
181 break
182 case "removeCollection":
183 execute(mc, removeCollection)
184 break
185 case "ensureFloatIndex":
186 execute(mc, ensureFloatIndex)
187 break
188 case "ensureStringIndex":
189 execute(mc, ensureStringIndex)
190 break
191 case "ensureIntIndex":
192 execute(mc, ensureIntIndex)
193 break
194 case "removeFloatIndex":
195 execute(mc, removeFloatIndex)
196 break
197 case "removeStringIndex":
198 execute(mc, removeStringIndex)
199 break
200 case "removeIntIndex":
201 execute(mc, removeIntIndex)
202 break
203 case "onlineBackup":
204 execute(mc, onlineBackup)
205 break
206 case "open":
207 execute(mc, open)
208 break
209 case "close":
210 execute(mc, close)
211 break
212 default:
213 NSLog("\nMethod call: \(call.method) not implemented")
214 result(FlutterMethodNotImplemented)
215 break
216 }
217 }
218
executeFirstnull219 func executeFirst(_ mc: DbMethodCall) throws {
220 return try executeListImpl(mc, 1)
221 }
222
executeListnull223 func executeList(_ mc: DbMethodCall) throws {
224 return try executeListImpl(mc)
225 }
226
executeListImplnull227 func executeListImpl(_ mc: DbMethodCall, _ limit: Int64? = nil) throws {
228 try prepareQuery(mc) { q, _ in
229 if limit != nil {
230 q.setLimit(limit!)
231 }
232 var res: [Any?] = []
233 try q.execute() { doc in
234 res.append(doc.id)
235 res.append(doc.json)
236 return true
237 }
238 mc.successOnMainThread(res)
239 }
240 }
241
executeScalarIntnull242 func executeScalarInt(_ mc: DbMethodCall) throws {
243 try prepareQuery(mc) { q, _ in
244 let res = try q.executeScalarInt()
245 mc.successOnMainThread(res)
246 }
247 }
248
executeQuerynull249 func executeQuery(_ mc: DbMethodCall) throws {
250 try prepareQuery(mc) { q, hook in
251 var batch: [Any?] = []
252 try q.execute() { doc in
253 if batch.count >= 128 && batch.count % 2 == 0 {
254 batch.insert(hook, at: 0)
255 let res = batch
256 DispatchQueue.main.sync {
257 self.events?(res)
258 }
259 batch = []
260 }
261 batch.append(doc.id)
262 batch.append(doc.json)
263 return true
264 }
265 batch.insert(hook, at: 0)
266 batch.append(true)
267 DispatchQueue.main.sync {
268 self.events?(batch)
269 }
270 }
271 }
272
prepareQuerynull273 func prepareQuery(_ mc: DbMethodCall, _ qh: DbQueryHandler) throws {
274 let db = mc.db
275 let hook = asString(mc.args[1])
276 let coll = asString(mc.args[2])
277 let qtext = asString(mc.args[3], "")!
278 let qspec = mc.args[4] as! [String: Any?]
279 let params = mc.args[5] as! [[Any?]]
280
281 let q = try db.createQuery(qtext, coll)
282 qspec.forEach { k, v in
283 switch (k) {
284 case "l": // limit
285 q.setLimit(asNumber(v)?.int64Value ?? 0)
286 break
287 case "s": // skip
288 q.setSkip(asNumber(v)?.int64Value ?? 0)
289 break
290 default:
291 break
292 }
293 }
294 for pslot in params {
295 let type = asNumber(pslot[0])?.intValue
296 let plh = pslot[1]
297 let val = pslot[2]
298 if type == 0 || val == nil {
299 if let v = plh as? NSNumber {
300 try q.setNull(v.int32Value)
301 } else {
302 try q.setNull(plh as! String)
303 }
304 } else {
305 switch (type) {
306 case 1: // String
307 if let v = plh as? NSNumber {
308 try q.setString(v.int32Value, asString(val))
309 } else {
310 try q.setString(plh as! String, asString(val))
311 }
312 break
313 case 2: // Long
314 if let v = plh as? NSNumber {
315 try q.setInt64(v.int32Value, asNumber(val)?.int64Value)
316 } else {
317 try q.setInt64(plh as! String, asNumber(val)?.int64Value)
318 }
319 break
320 case 3: // Double
321 if let v = plh as? NSNumber {
322 try q.setDouble(v.int32Value, asDouble(val))
323 } else {
324 try q.setDouble(plh as! String, asDouble(val))
325 }
326 break
327 case 4: // Boolean
328 if let v = plh as? NSNumber {
329 try q.setBool(v.int32Value, asBoolean(val))
330 } else {
331 try q.setBool(plh as! String, asBoolean(val))
332 }
333 break
334 case 5: // Regexp
335 if let v = plh as? NSNumber {
336 try q.setRegexp(v.int32Value, asString(val))
337 } else {
338 try q.setRegexp(plh as! String, asString(val))
339 }
340 break
341 case 6: // JSON
342 if let v = plh as? NSNumber {
343 try q.setJson(v.int32Value, val!)
344 } else {
345 try q.setJson(plh as! String, val!)
346 }
347 break
348 default:
349 break
350 }
351 }
352 }
353 try qh(q, hook)
354 }
355
onlineBackupnull356 func onlineBackup(_ mc: DbMethodCall) throws {
357 var path = Path(asString(mc.args[1])!)
358 if !path.isAbsolute {
359 let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
360 path = Path(documentsPath) + path
361 }
362 NSLog("\nOnline backup into: \(path)")
363 mc.successOnMainThread(try mc.db.onlineBackup(path.string))
364 }
365
removeFloatIndexnull366 func removeFloatIndex(_ mc: DbMethodCall) throws {
367 let coll = asString(mc.args[1])!
368 let path = asString(mc.args[2])!
369 let unique = asBoolean(mc.args[3])
370 try mc.db.removeFloatIndex(coll, path, unique: unique)
371 mc.successOnMainThread()
372 }
373
ensureFloatIndexnull374 func ensureFloatIndex(_ mc: DbMethodCall) throws {
375 let coll = asString(mc.args[1])!
376 let path = asString(mc.args[2])!
377 let unique = asBoolean(mc.args[3])
378 try mc.db.ensureFloatIndex(coll, path, unique: unique)
379 mc.successOnMainThread()
380 }
381
removeIntIndexnull382 func removeIntIndex(_ mc: DbMethodCall) throws {
383 let coll = asString(mc.args[1])!
384 let path = asString(mc.args[2])!
385 let unique = asBoolean(mc.args[3])
386 try mc.db.removeIntIndex(coll, path, unique: unique)
387 mc.successOnMainThread()
388 }
389
ensureIntIndexnull390 func ensureIntIndex(_ mc: DbMethodCall) throws {
391 let coll = asString(mc.args[1])!
392 let path = asString(mc.args[2])!
393 let unique = asBoolean(mc.args[3])
394 try mc.db.ensureIntIndex(coll, path, unique: unique)
395 mc.successOnMainThread()
396 }
397
removeStringIndexnull398 func removeStringIndex(_ mc: DbMethodCall) throws {
399 let coll = asString(mc.args[1])!
400 let path = asString(mc.args[2])!
401 let unique = asBoolean(mc.args[3])
402 try mc.db.removeStringIndex(coll, path, unique: unique)
403 mc.successOnMainThread()
404 }
405
ensureStringIndexnull406 func ensureStringIndex(_ mc: DbMethodCall) throws {
407 let coll = asString(mc.args[1])!
408 let path = asString(mc.args[2])!
409 let unique = asBoolean(mc.args[3])
410 try mc.db.ensureStringIndex(coll, path, unique: unique)
411 mc.successOnMainThread()
412 }
413
removeCollectionnull414 func removeCollection(_ mc: DbMethodCall) throws {
415 let coll = asString(mc.args[1])!
416 try mc.db.removeCollection(coll)
417 mc.successOnMainThread()
418 }
419
renameCollectionnull420 func renameCollection(_ mc: DbMethodCall) throws {
421 let oldName = asString(mc.args[1])!
422 let newName = asString(mc.args[2])!
423 try mc.db.renameCollection(oldName, newName)
424 mc.successOnMainThread()
425 }
426
delnull427 func del(_ mc: DbMethodCall) throws {
428 let coll = asString(mc.args[1])!
429 let id = asNumber(mc.args[2])!.int64Value
430 try mc.db.del(coll, id)
431 mc.successOnMainThread()
432 }
433
patchnull434 func patch(_ mc: DbMethodCall) throws {
435 let db = mc.db
436 let coll = asString(mc.args[1])!
437 let json = asString(mc.args[2])!
438 let id = asNumber(mc.args[3])!.int64Value
439 let upsert = asBoolean(mc.args[4])
440 if upsert {
441 try db.put(coll, json, id, merge: true)
442 } else {
443 try db.patch(coll, json, id)
444 }
445 mc.successOnMainThread()
446 }
447
getnull448 func get(_ mc: DbMethodCall) throws {
449 let db = mc.db
450 let coll = asString(mc.args[1])!
451 let id = asNumber(mc.args[2])!.int64Value
452 let doc = try db.get(coll, id)
453 mc.successOnMainThread(doc.json)
454 }
455
putnull456 func put(_ mc: DbMethodCall) throws {
457 let db = mc.db
458 let coll = asString(mc.args[1])!
459 let json = asString(mc.args[2])!
460 let id = asNumber(mc.args[3])?.int64Value ?? 0
461 let res = try db.put(coll, json, id)
462 mc.successOnMainThread(res)
463 }
464
infonull465 func info(_ mc: DbMethodCall) throws {
466 let info = try mc.db.info()
467 let data = String(data: try JSONEncoder().encode(info), encoding: .utf8)
468 mc.successOnMainThread(data)
469 }
470
opennull471 func open(_ mc: DbMethodCall) throws {
472 var path = Path(mc.args[1] as! String)
473 if !path.isAbsolute {
474 let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
475 path = Path(documentsPath) + path
476 }
477 path = path.normalize()
478 let dbe = dbmap.values.first(where: { $0.path == path.string })
479 if dbe != nil {
480 dbe!.countOpen()
481 mc.successOnMainThread(dbe!.handle)
482 return
483 }
484 let cfg = mc.args[2] as? [String: Any] ?? [:]
485 var b = EJDB2Builder(path.string)
486 if cfg.containsKey("truncate") {
487 b = b.withTruncate(asBoolean(cfg["truncate"]))
488 }
489 if cfg.containsKey("readonly") {
490 b = b.withReadonly(asBoolean(cfg["readonly"]))
491 }
492 if cfg.containsKey("wal_enabled") {
493 b.withWalDisabled(!asBoolean(cfg["wal_enabled"], true))
494 }
495 if cfg.containsKey("wal_check_crc_on_checkpoint") {
496 b.withWalCheckCRCOnCheckpoint(asBoolean(cfg["wal_check_crc_on_checkpoint"]))
497 }
498 if cfg.containsKey("wal_checkpoint_buffer_sz") {
499 b.withWalCheckpointBufferSize(asNumber(cfg["wal_checkpoint_buffer_sz"])?.uint64Value ?? 0)
500 }
501 if cfg.containsKey("wal_checkpoint_timeout_sec") {
502 b.withWalCheckpointTimeout(asNumber(cfg["wal_checkpoint_timeout_sec"])?.uint32Value ?? 0)
503 }
504 if cfg.containsKey("wal_savepoint_timeout_sec") {
505 b.withWalSavepointTimeout(asNumber(cfg["wal_checkpoint_timeout_sec"])?.uint32Value ?? 0)
506 }
507 if cfg.containsKey("wal_wal_buffer_sz") {
508 b.withWalBufferSize(asNumber(cfg["wal_wal_buffer_sz"])?.intValue ?? 0)
509 }
510 if cfg.containsKey("document_buffer_sz") {
511 b.withDocumentBufferSize(asNumber(cfg["document_buffer_sz"])?.uint32Value ?? 0)
512 }
513 if cfg.containsKey("sort_buffer_sz") {
514 b.withSortBufferSize(asNumber(cfg["sort_buffer_sz"])?.uint32Value ?? 0)
515 }
516 dbkeys += 1
517 let key = dbkeys
518 dbmap[key] = try DbEntry(b.open(), key, path.string)
519 mc.successOnMainThread(key as NSNumber)
520 }
521
closenull522 func close(_ mc: DbMethodCall) throws {
523 if try mc.dbe?.close() ?? false {
524 dbmap.removeValue(forKey: mc.dbe?.handle ?? 0)
525 }
526 mc.successOnMainThread()
527 }
528
onListennull529 public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
530 self.events = events
531 return nil
532 }
533
onCancelnull534 public func onCancel(withArguments arguments: Any?) -> FlutterError? {
535 self.events = nil
536 return nil
537 }
538 }
539