1// @dart=2.5 2 3import 'dart:async'; 4import 'dart:convert' as convert_lib; 5 6import 'package:flutter/services.dart'; 7import 'package:json_at/json_at.dart'; 8import 'package:pedantic/pedantic.dart'; 9import 'package:quiver/core.dart'; 10 11const MethodChannel _mc = MethodChannel('ejdb2'); 12 13const EventChannel _qc = EventChannel('ejdb2/query'); 14 15Map<String, StreamController<JBDOC>> _ecm; 16 17void _execute(String hook, dynamic args, StreamController<JBDOC> sctl) { 18 if (_ecm == null) { 19 _ecm = {}; 20 _qc.receiveBroadcastStream().listen(_onQueryData); 21 } 22 unawaited(_mc.invokeMethod('executeQuery', args).catchError((err, StackTrace s) { 23 _ecm.remove(hook); 24 dynamic streamError = err; 25 if (err is PlatformException && err.code.startsWith('@ejdb')) { 26 streamError = EJDB2Error(err.code, err.message); 27 } 28 sctl.addError(streamError, s); 29 })); 30 _ecm[hook] = sctl; 31} 32 33void _onQueryData(dynamic dataArg) { 34 final data = dataArg as List; 35 if (data.isEmpty) { 36 return; 37 } 38 final qhook = data[0] as String; 39 final sctl = _ecm[qhook]; 40 if (sctl == null) { 41 return; 42 } 43 final last = data.last == true; 44 final l = last ? data.length - 1 : data.length; 45 for (int i = 1; i < l; i += 2) { 46 sctl.add(JBDOC(data[i] as int, data[i + 1] as String)); 47 } 48 if (last) { 49 _ecm.remove(qhook); 50 sctl.close(); 51 } 52} 53 54/// EJDB2 Instance builder. 55/// 56class EJDB2Builder { 57 EJDB2Builder(this.path); 58 59 final String path; 60 61 bool readonly; 62 63 bool truncate; 64 65 bool walEnabled; 66 67 bool walCheckCRCOnCheckpoint; 68 69 int walCheckpointBufferSize; 70 71 int walCheckpointTimeout; 72 73 int walSavepointTimeout; 74 75 int walBufferSize; 76 77 int documentBufferSize; 78 79 int sortBufferSize; 80 81 /// Open EJDB2 database 82 /// See https://github.com/Softmotions/ejdb/blob/master/src/ejdb2.h#L104 83 /// for description of options. 84 Future<EJDB2> open() => EJDB2._open(this); 85 86 Map<String, dynamic> _asOpts() => <String, dynamic>{ 87 'readonly': readonly ?? false, 88 'truncate': truncate ?? false, 89 'wal_enabled': walEnabled ?? true, 90 'wal_check_crc_on_checkpoint': walCheckCRCOnCheckpoint ?? false, 91 ...walCheckpointBufferSize != null 92 ? {'wal_checkpoint_buffer_sz': walCheckpointBufferSize} 93 : {}, 94 ...walCheckpointTimeout != null ? {'wal_checkpoint_timeout_sec': walCheckpointTimeout} : {}, 95 ...walSavepointTimeout != null ? {'wal_savepoint_timeout_sec': walSavepointTimeout} : {}, 96 ...walBufferSize != null ? {'wal_wal_buffer_sz': walBufferSize} : {}, 97 ...documentBufferSize != null ? {'document_buffer_sz': documentBufferSize} : {}, 98 ...sortBufferSize != null ? {'sort_buffer_sz': sortBufferSize} : {} 99 }; 100} 101 102FutureOr<T> Function(Object, StackTrace) _errorHandler<T>() => 103 (Object err, StackTrace s) { 104 if (err is PlatformException && err.code.startsWith('@ejdb')) { 105 return Future<T>.error(EJDB2Error(err.code, err.message), s); 106 } 107 return Future<T>.error(err, s); 108 }; 109 110class EJDB2Error implements Exception { 111 EJDB2Error(this.code, this.message); 112 EJDB2Error.notFound() : this('@ejdb IWRC:75001', 'Not found'); 113 114 final String code; 115 final String message; 116 117 bool isNotFound() => (code ?? '').startsWith('@ejdb IWRC:75001'); 118 119 bool isInvalidQuery() => (code ?? '').startsWith('@ejdb IWRC:87001'); 120 121 @override 122 String toString() => '$runtimeType: $code $message'; 123} 124 125/// EJDB document item. 126/// 127class JBDOC { 128 JBDOC(this.id, this._json); 129 JBDOC._fromList(List list) : this(list[0] as int, list[1] as String); 130 131 /// Document identifier 132 final int id; 133 134 /// Document body as JSON string 135 String get json => _json ?? convert_lib.jsonEncode(_object); 136 137 /// Document body as parsed JSON object. 138 dynamic get object { 139 if (_json == null) { 140 return _object; 141 } else { 142 _object = convert_lib.jsonDecode(_json); 143 _json = null; // Release memory used to store JSON string data 144 return _object; 145 } 146 } 147 148 /// Gets subset of document using RFC 6901 JSON [pointer]. 149 Optional<dynamic> at(String pointer) => jsonAt(object, pointer); 150 151 /// Gets subset of document using RFC 6901 JSON [pointer]. 152 Optional<dynamic> operator [](String pointer) => at(pointer); 153 154 String _json; 155 156 dynamic _object; 157 158 @override 159 String toString() => '$runtimeType: $id $json'; 160} 161 162/// EJDB2 query builder/executor. 163/// 164class JQL { 165 JQL._(this._jb, this.collection, this.qtext); 166 167 static int _qhandle = 0; 168 169 final EJDB2 _jb; 170 171 final _qspec = <String, dynamic>{}; 172 173 final _params = <dynamic>[]; 174 175 final String collection; 176 177 final String qtext; 178 179 int get limit => _qspec['l'] as int ?? 0; 180 181 set limit(int limit) => _qspec['l'] = limit; 182 183 int get skip => _qspec['s'] as int ?? 0; 184 185 set skip(int skip) => _qspec['s'] = skip; 186 187 int get _handle => _jb._handle; 188 189 /// Set string [val] at the specified [placeholder]. 190 /// [placeholder] can be either `string` or `int` 191 JQL setString(dynamic placeholder, String val) { 192 _checkPlaceholder(placeholder); 193 ArgumentError.checkNotNull(val); 194 _params.add([1, placeholder, val]); 195 return this; 196 } 197 198 /// Set integer [val] at the specified [placeholder]. 199 /// [placeholder] can be either `string` or `int` 200 JQL setInt(dynamic placeholder, int val) { 201 _checkPlaceholder(placeholder); 202 ArgumentError.checkNotNull(val); 203 _params.add([2, placeholder, val]); 204 return this; 205 } 206 207 /// Set double [val] at the specified [placeholder]. 208 /// [placeholder] can be either `string` or `int` 209 JQL setDouble(dynamic placeholder, double val) { 210 _checkPlaceholder(placeholder); 211 ArgumentError.checkNotNull(val); 212 _params.add([3, placeholder, val]); 213 return this; 214 } 215 216 /// Set bool [val] at the specified [placeholder]. 217 /// [placeholder] can be either `string` or `int` 218 JQL setBoolean(dynamic placeholder, bool val) { 219 _checkPlaceholder(placeholder); 220 ArgumentError.checkNotNull(val); 221 _params.add([4, placeholder, val]); 222 return this; 223 } 224 225 /// Set RegExp [val] at the specified [placeholder]. 226 /// [placeholder] can be either `string` or `int` 227 JQL setRegexp(dynamic placeholder, RegExp val) { 228 _checkPlaceholder(placeholder); 229 ArgumentError.checkNotNull(val); 230 _params.add([5, placeholder, val.pattern]); 231 return this; 232 } 233 234 /// Set [json] at the specified [placeholder]. 235 /// [placeholder] can be either `string` or `int` 236 JQL setJson(dynamic placeholder, Object json) { 237 _checkPlaceholder(placeholder); 238 ArgumentError.checkNotNull(json); 239 if (json is! String) { 240 json = convert_lib.jsonEncode(json); 241 } 242 _params.add([6, placeholder, json]); 243 return this; 244 } 245 246 /// Execute query and returns a stream of matched documents. 247 Stream<JBDOC> execute() { 248 final qh = '${++JQL._qhandle}'; 249 final sctl = StreamController<JBDOC>(onCancel: () { 250 _ecm.remove(qh); 251 }); 252 final args = [_handle, qh, collection, qtext, _qspec, _params]; 253 _execute(qh, args, sctl); 254 return sctl.stream; 255 } 256 257 /// Executes query then listen event stream for first event/error 258 /// to eagerly fetch pending error if available. 259 Future<void> executeTouch() { 260 StreamSubscription subscription; 261 final Completer completer = Completer(); 262 final stream = execute(); 263 subscription = stream.listen((val) { 264 if (!completer.isCompleted) { 265 completer.complete(); 266 } 267 final sf = subscription.cancel(); 268 if (sf != null) { 269 unawaited(sf.catchError(() {})); 270 } 271 }, onError: (e, StackTrace s) { 272 if (!completer.isCompleted) { 273 completer.completeError(e, s); 274 } 275 }, onDone: () { 276 if (!completer.isCompleted) { 277 completer.complete(); 278 } 279 }, cancelOnError: true); 280 return completer.future; 281 } 282 283 /// Return scalar integer value as result of query execution. 284 /// For example execution of count query: `/... | count` 285 Future<int> executeScalarInt() => _mc.invokeMethod('executeScalarInt', 286 [_handle, null, collection, qtext, _qspec, _params]).then((v) => v as int); 287 288 /// Returns list of matched documents. 289 /// Use it with care to avoid wasting of memory. 290 Future<List<JBDOC>> list([int limitArg]) async { 291 var qspec = _qspec; 292 if (limitArg != null) { 293 // Copy on write to be safe 294 qspec = { 295 ..._qspec, 296 ...{'l': limitArg} 297 }; 298 } 299 final list = await _mc 300 .invokeListMethod('executeList', [_handle, null, collection, qtext, qspec, _params]); 301 final res = <JBDOC>[]; 302 for (int i = 0; i < list.length; i += 2) { 303 res.add(JBDOC(list[i] as int, list[i + 1] as String)); 304 } 305 return res; 306 } 307 308 /// Returns optional element for first record in result set. 309 Future<Optional<JBDOC>> first() async { 310 final l = await list(limit = 1); 311 return l.isNotEmpty ? Optional.of(l.first) : const Optional.absent(); 312 } 313 314 /// Return first record in result set or resolve to notfound [EJDB2Error] error. 315 Future<JBDOC> firstRequired() async { 316 final f = await first(); 317 if (f.isPresent) { 318 return f.value; 319 } 320 return Future.error(EJDB2Error.notFound()); 321 } 322 323 /// Returns list of first [n] matched documents. 324 /// Use it with care to avoid wasting of memory. 325 Future<List<JBDOC>> firstN(int n) => list(n); 326 327 void _checkPlaceholder(dynamic placeholder) { 328 if (!(placeholder is String) && !(placeholder is int)) { 329 ArgumentError.value(placeholder, 'placeholder'); 330 } 331 } 332} 333 334class EJDB2 { 335 EJDB2._(this._handle); 336 337 final int _handle; 338 339 static Future<EJDB2> _open(EJDB2Builder b) async { 340 final hdl = 341 await _mc.invokeMethod<int>('open', [null, b.path, b._asOpts()]).catchError(_errorHandler<int>()); 342 return EJDB2._(hdl); 343 } 344 345 /// Closes database instance. 346 Future<void> close() { 347 if (_handle == null) { 348 throw StateError('Closed'); 349 } 350 return _mc.invokeMethod('close', [_handle]).catchError(_errorHandler()); 351 } 352 353 /// Create instance of [query] specified for [collection]. 354 /// If [collection] is not specified a [query] spec must contain collection name, 355 /// eg: `@mycollection/[foo=bar]` 356 JQL createQuery(String query, [String collection]) => JQL._(this, collection, query); 357 358 /// Create instance of [query]. 359 JQL operator [](String query) => createQuery(query); 360 361 /// Save [json] document under specified [id] or create a new document 362 /// with new generated `id`. Returns future holding actual document `id`. 363 Future<int> put(String collection, dynamic json, [int id]) => Future.sync(() => _mc 364 .invokeMethod('put', [ 365 _handle, 366 collection, 367 (json is! String) ? convert_lib.jsonEncode(json) : json as String, 368 id 369 ]) 370 .catchError(_errorHandler<int>()) 371 .then((v) => v as int)); 372 373 /// Apply rfc6902/rfc6901 JSON [patch] to the document identified by [id]. 374 Future<void> patch(String collection, dynamic json, int id, [bool upsert = false]) => 375 Future.sync(() => _mc.invokeMethod('patch', [ 376 _handle, 377 collection, 378 (json is! String) ? convert_lib.jsonEncode(json) : json as String, 379 id, 380 upsert 381 ]).catchError(_errorHandler())); 382 383 /// Apply JSON merge patch (rfc7396) to the document identified by `id` or 384 /// insert new document under specified `id`. 385 Future<void> patchOrPut(String collection, dynamic json, int id) => 386 patch(collection, json, id, true); 387 388 /// Get json body of document identified by [id] and stored in [collection]. 389 /// Throws [EJDB2Error] not found exception if document is not found. 390 Future<JBDOC> get(String collection, int id) => _mc 391 .invokeMethod('get', [_handle, collection, id]) 392 .catchError(_errorHandler<JBDOC>()) 393 .then((v) => JBDOC(id, v as String)); 394 395 /// Get json body of document identified by [id] and stored in [collection]. 396 Future<Optional<JBDOC>> getOptional(String collection, int id) { 397 return get(collection, id).then((doc) => Optional.of(doc)).catchError((err) { 398 if (err is EJDB2Error && err.isNotFound()) { 399 return Future.value(const Optional<JBDOC>.absent()); 400 } 401 return Future.error(err); 402 }); 403 } 404 405 /// Remove document identified by [id] from specified [collection]. 406 Future<void> del(String collection, int id) => 407 _mc.invokeMethod('del', [_handle, collection, id]).catchError(_errorHandler()); 408 409 /// Remove document identified by [id] from specified [collection]. 410 /// Doesn't raise error if document is not found. 411 Future<void> delIgnoreNotFound(String collection, int id) => 412 del(collection, id).catchError((err) { 413 if (err is EJDB2Error && err.isNotFound()) { 414 return Future.value(); 415 } else { 416 return Future.error(err); 417 } 418 }); 419 420 /// Get json body of database metadata. 421 Future<dynamic> info() => _mc 422 .invokeMethod('info', [_handle]) 423 .catchError(_errorHandler()) 424 .then((v) => convert_lib.jsonDecode(v as String)); 425 426 /// Removes database [collection]. 427 Future<void> removeCollection(String collection) => 428 _mc.invokeMethod('removeCollection', [_handle, collection]).catchError(_errorHandler()); 429 430 /// Renames database collection. 431 Future<void> renameCollection(String oldName, String newName) { 432 return _mc 433 .invokeMethod('renameCollection', [_handle, oldName, newName]).catchError(_errorHandler()); 434 } 435 436 Future<void> ensureStringIndex(String coll, String path, [bool unique]) => 437 _mc.invokeMethod('ensureStringIndex', [_handle, coll, path, unique ?? false]); 438 439 Future<void> removeStringIndex(String coll, String path, [bool unique]) => 440 _mc.invokeMethod('removeStringIndex', [_handle, coll, path, unique ?? false]); 441 442 Future<void> ensureIntIndex(String coll, String path, [bool unique]) => 443 _mc.invokeMethod('ensureIntIndex', [_handle, coll, path, unique ?? false]); 444 445 Future<void> removeIntIndex(String coll, String path, [bool unique]) => 446 _mc.invokeMethod('removeIntIndex', [_handle, coll, path, unique ?? false]); 447 448 Future<void> ensureFloatIndex(String coll, String path, [bool unique]) => 449 _mc.invokeMethod('ensureFloatIndex', [_handle, coll, path, unique ?? false]); 450 451 Future<void> removeFloatIndex(String coll, String path, [bool unique]) => 452 _mc.invokeMethod('removeFloatIndex', [_handle, coll, path, unique ?? false]); 453 454 /// Creates an online database backup image and copies it into the specified [fileName]. 455 /// During online backup phase read/write database operations are allowed and not 456 /// blocked for significant amount of time. Returns future with backup 457 /// finish time as number of milliseconds since epoch. 458 Future<int> onlineBackup(String fileName) => 459 _mc.invokeMethod('onlineBackup', [_handle, fileName]).then((v) => v as int); 460} 461