• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1///
2/// EJDB2 Dart VM native API binding.
3///
4/// See https://github.com/Softmotions/ejdb/blob/master/README.md
5///
6/// For API usage examples look into `/example` and `/test` folders.
7///
8
9library ejdb2_dart;
10
11import 'dart:async';
12import 'dart:convert';
13import 'dart:convert' as convert_lib;
14import 'dart:io';
15import 'dart:isolate';
16import 'dart:nativewrappers' show NativeFieldWrapperClass2;
17
18import 'package:path/path.dart' as path_lib;
19import 'package:quiver/core.dart';
20import 'package:json_at/json_at.dart';
21
22import 'dart-ext:ejdb2dart';
23
24String ejdb2ExplainRC(int rc) native 'explain_rc';
25
26/// EJDB specific exception
27class EJDB2Error implements Exception {
28  EJDB2Error(this.code, this.message);
29
30  EJDB2Error.fromCode(int code) : this(code, ejdb2ExplainRC(code));
31
32  EJDB2Error.invalidState() : this.fromCode(EJD_ERROR_INVALID_STATE);
33
34  EJDB2Error.notFound() : this.fromCode(IWKV_ERROR_NOTFOUND);
35
36  static int EJD_ERROR_CREATE_PORT = 89001;
37  static int EJD_ERROR_POST_PORT = 89002;
38  static int EJD_ERROR_INVALID_NATIVE_CALL_ARGS = 89003;
39  static int EJD_ERROR_INVALID_STATE = 89004;
40  static int IWKV_ERROR_NOTFOUND = 75001;
41
42  final int code;
43
44  final String message;
45
46  bool get notFound => code == IWKV_ERROR_NOTFOUND;
47
48  bool get invalidQuery => code == 87001;
49
50  @override
51  String toString() => '$runtimeType: $code $message';
52}
53
54/// EJDB document item
55class JBDOC {
56  JBDOC(this.id, this._json);
57  JBDOC._fromList(List list) : this(list[0] as int, list[1] as String?);
58
59  /// Document identifier
60  final int id;
61
62  /// Document body as JSON string
63  String get json => _json ?? convert_lib.jsonEncode(_object);
64
65  /// Document body as parsed JSON object.
66  dynamic get object {
67    if (_json == null) {
68      return _object;
69    } else {
70      _object = convert_lib.jsonDecode(_json!);
71      _json = null; // Release memory used to store JSON string data
72      return _object;
73    }
74  }
75
76  /// Gets subset of document using RFC 6901 JSON [pointer].
77  Optional<dynamic> at(String pointer) => jsonAt(object, pointer);
78
79  /// Gets subset of document using RFC 6901 JSON [pointer].
80  Optional<dynamic> operator [](String pointer) => at(pointer);
81
82  String? _json;
83
84  dynamic _object;
85
86  @override
87  String toString() => '$runtimeType: $id $json';
88}
89
90/// Represents query on ejdb collection.
91/// Instance can be reused for multiple queries reusing
92/// placeholder parameters.
93class JQL extends NativeFieldWrapperClass2 {
94  JQL._(this.db, this.query, this.collection);
95
96  final String query;
97  final String collection;
98  final EJDB2 db;
99
100  StreamController<JBDOC>? _controller;
101  RawReceivePort? _replyPort;
102
103  /// Execute query and returns a stream of matched documents.
104  ///
105  /// [explainCallback] Used to get query execution log.
106  /// [limit] Overrides `limit` set by query text for this execution session.
107  ///
108  Stream<JBDOC> execute({void explainCallback(String log)?, int limit = 0}) {
109    abort();
110    var execHandle = 0;
111    _controller = StreamController<JBDOC>();
112    _replyPort = RawReceivePort();
113    _replyPort!.handler = (dynamic reply) {
114      if (reply is int) {
115        _exec_check(execHandle, true);
116        _replyPort!.close();
117        _controller!.addError(EJDB2Error.fromCode(reply));
118        return;
119      } else if (reply is List) {
120        _exec_check(execHandle, false);
121        if (reply[2] != null && explainCallback != null) {
122          explainCallback(reply[2] as String);
123        }
124        _controller!.add(JBDOC._fromList(reply));
125      } else {
126        _exec_check(execHandle, true);
127        if (reply != null && explainCallback != null) {
128          explainCallback(reply as String);
129        }
130        abort();
131      }
132    };
133    execHandle = _exec(_replyPort!.sendPort, explainCallback != null, limit);
134    return _controller!.stream;
135  }
136
137  /// Returns optional element for first record in result set.
138  Future<Optional<JBDOC>> first({void explainCallback(String log)?}) async {
139    await for (final doc in execute(explainCallback: explainCallback, limit: 1)) {
140      return Optional.of(doc);
141    }
142    return const Optional.absent();
143  }
144
145  /// Return first record in result set or throw not found [EJDB2Error] error.
146  Future<JBDOC> firstRequired({void explainCallback(String log)?}) async {
147    await for (final doc in execute(explainCallback: explainCallback, limit: 1)) {
148      return doc;
149    }
150    throw EJDB2Error.notFound();
151  }
152
153  /// Collects up to [n] elements from result set into array.
154  Future<List<JBDOC>> firstN(int n, {void explainCallback(String log)?}) async {
155    final ret = <JBDOC>[];
156    await for (final doc in execute(explainCallback: explainCallback, limit: n)) {
157      if (n-- <= 0) break;
158      ret.add(doc);
159    }
160    return ret;
161  }
162
163  /// Abort query execution.
164  void abort() {
165    _replyPort?.close();
166    _replyPort = null;
167    _controller?.close();
168    _controller = null;
169  }
170
171  /// Return scalar integer value as result of query execution.
172  /// For example execution of count query: `/... | count`
173  Future<int> scalarInt({void explainCallback(String log)?}) {
174    return execute(explainCallback: explainCallback).map((d) => d.id).first;
175  }
176
177  /// Set [json] at the specified [placeholder].
178  /// [placeholder] can be either `string` or `int`
179  JQL setJson(dynamic placeholder, dynamic json) {
180    _checkPlaceholder(placeholder);
181    ArgumentError.checkNotNull(json);
182    _set(placeholder, _asJsonString(json), 1);
183    return this;
184  }
185
186  /// Set [regexp] at the specified [placeholder].
187  /// [placeholder] can be either `string` or `int`
188  JQL setRegExp(dynamic placeholder, RegExp regexp) {
189    _checkPlaceholder(placeholder);
190    ArgumentError.checkNotNull(regexp);
191    _set(placeholder, regexp.pattern, 2);
192    return this;
193  }
194
195  /// Set integer [val] at the specified [placeholder].
196  /// [placeholder] can be either `string` or `int`
197  JQL setInt(dynamic placeholder, int val) {
198    _checkPlaceholder(placeholder);
199    ArgumentError.checkNotNull(val);
200    _set(placeholder, val);
201    return this;
202  }
203
204  /// Set double [val] at the specified [placeholder].
205  /// [placeholder] can be either `string` or `int`
206  JQL setDouble(dynamic placeholder, double val) {
207    _checkPlaceholder(placeholder);
208    ArgumentError.checkNotNull(val);
209    _set(placeholder, val);
210    return this;
211  }
212
213  /// Set boolean [val] at the specified [placeholder].
214  /// [placeholder] can be either `string` or `int`
215  JQL setBoolean(dynamic placeholder, bool val) {
216    _checkPlaceholder(placeholder);
217    ArgumentError.checkNotNull(val);
218    _set(placeholder, val);
219    return this;
220  }
221
222  /// Set string [val] at the specified [placeholder].
223  /// [placeholder] can be either `string` or `int`
224  JQL setString(dynamic placeholder, String val) {
225    _checkPlaceholder(placeholder);
226    ArgumentError.checkNotNull(val);
227    _set(placeholder, val);
228    return this;
229  }
230
231  /// Set `null` at the specified [placeholder].
232  /// [placeholder] can be either `string` or `int`
233  JQL setNull(dynamic placeholder) {
234    _checkPlaceholder(placeholder);
235    _set(placeholder, null);
236    return this;
237  }
238
239  /// Get current `limit` encoded in query.
240  int get limit native 'jql_get_limit';
241
242  void _checkPlaceholder(dynamic placeholder) {
243    if (!(placeholder is String) && !(placeholder is int)) {
244      ArgumentError.value(placeholder, 'placeholder');
245    }
246  }
247
248  void _set(dynamic placeholder, dynamic value, [int type]) native 'jql_set';
249
250  int _exec(SendPort sendPort, bool explain, int limit) native 'exec';
251
252  void _exec_check(int execHandle, bool terminate) native 'check_exec';
253}
254
255/// Database wrapper
256class EJDB2 extends NativeFieldWrapperClass2 {
257  EJDB2._();
258
259  static bool _checkCompleterPortError(Completer<dynamic> completer, dynamic reply) {
260    if (reply is int) {
261      completer.completeError(EJDB2Error(reply, ejdb2ExplainRC(reply)));
262      return true;
263    } else if (reply is! List) {
264      completer.completeError(EJDB2Error(0, 'Invalid port response'));
265      return true;
266    }
267    return false;
268  }
269
270  /// Open EJDB2 database
271  /// See https://github.com/Softmotions/ejdb/blob/master/src/ejdb2.h#L104
272  /// for description of options.
273  static Future<EJDB2> open(String path,
274      {bool truncate = false,
275      bool readonly = false,
276      bool http_enabled = false,
277      bool http_read_anon = false,
278      bool wal_enabled = true,
279      bool wal_check_crc_on_checkpoint = false,
280      int? wal_checkpoint_buffer_sz,
281      int? wal_checkpoint_timeout_sec,
282      int? wal_savepoint_timeout_sec,
283      int? wal_wal_buffer_sz,
284      int? document_buffer_sz,
285      int? sort_buffer_sz,
286      String? http_access_token,
287      String? http_bind,
288      int? http_max_body_size,
289      int? http_port}) {
290    final completer = Completer<EJDB2>();
291    final replyPort = RawReceivePort();
292    final jb = EJDB2._();
293
294    path = path_lib.canonicalize(File(path).absolute.path);
295
296    replyPort.handler = (dynamic reply) {
297      replyPort.close();
298      if (_checkCompleterPortError(completer, reply)) {
299        return;
300      }
301      try {
302        jb._set_handle((reply as List).first as int);
303      } catch (e) {
304        completer.completeError(e);
305        return;
306      }
307      completer.complete(jb);
308    };
309
310    // Open
311    var oflags = 0;
312    if (readonly) {
313      oflags |= 0x02;
314    } else if (truncate) {
315      oflags |= 0x04;
316    }
317
318    jb._port().send([
319      replyPort.sendPort,
320      'open',
321      path,
322      oflags,
323      wal_enabled,
324      wal_check_crc_on_checkpoint,
325      wal_checkpoint_buffer_sz as dynamic,
326      wal_checkpoint_timeout_sec as dynamic,
327      wal_savepoint_timeout_sec as dynamic,
328      wal_wal_buffer_sz as dynamic,
329      document_buffer_sz as dynamic,
330      sort_buffer_sz as dynamic,
331      http_enabled,
332      http_access_token as dynamic,
333      http_bind as dynamic,
334      http_max_body_size as dynamic,
335      http_port as dynamic,
336      http_read_anon
337    ]);
338    return completer.future;
339  }
340
341  /// Closes database instance.
342  Future<void> close() {
343    final hdb = _get_handle();
344    if (hdb == null) {
345      return Future.value();
346    }
347    final completer = Completer<void>();
348    final replyPort = RawReceivePort();
349    replyPort.handler = (dynamic reply) {
350      replyPort.close();
351      if (_checkCompleterPortError(completer, reply)) {
352        return;
353      }
354      completer.complete();
355    };
356    _set_handle(null);
357    _port().send([replyPort.sendPort, 'close', hdb as dynamic]);
358    return completer.future;
359  }
360
361  /// Save [json] document under specified [id] or create a document
362  /// with new generated `id`. Returns future holding actual document `id`.
363  Future<int> put(String collection, dynamic json, [int? id]) {
364    final hdb = _get_handle();
365    if (hdb == null) {
366      return Future.error(EJDB2Error.invalidState());
367    }
368    final completer = Completer<int>();
369    final replyPort = RawReceivePort();
370    replyPort.handler = (dynamic reply) {
371      replyPort.close();
372      if (_checkCompleterPortError(completer, reply)) {
373        return;
374      }
375      completer.complete((reply as List).first as int);
376    };
377    _port().send([
378      replyPort.sendPort,
379      'put',
380      hdb as dynamic,
381      collection,
382      _asJsonString(json),
383      id as dynamic
384    ]);
385    return completer.future;
386  }
387
388  /// Apply rfc6902/rfc7386 JSON [patch] to the document identified by [id].
389  Future<void> patch(String collection, dynamic patchObj, int id, [bool upsert = false]) {
390    final hdb = _get_handle();
391    if (hdb == null) {
392      return Future.error(EJDB2Error.invalidState());
393    }
394    final completer = Completer<void>();
395    final replyPort = RawReceivePort();
396    replyPort.handler = (dynamic reply) {
397      replyPort.close();
398      if (_checkCompleterPortError(completer, reply)) {
399        return;
400      }
401      completer.complete();
402    };
403    _port().send([
404      replyPort.sendPort,
405      'patch',
406      hdb as dynamic,
407      collection,
408      _asJsonString(patchObj),
409      id as dynamic,
410      upsert
411    ]);
412    return completer.future;
413  }
414
415  /// Apply JSON merge patch (rfc7396) to the document identified by `id` or
416  /// insert new document under specified `id`.
417  Future<void> patchOrPut(String collection, dynamic patchObj, int id) =>
418      patch(collection, patch, id, true);
419
420  /// Get json body of document identified by [id] and stored in [collection].
421  /// Throws [EJDB2Error] not found exception if document is not found.
422  Future<String> get(String collection, int id) {
423    final hdb = _get_handle();
424    if (hdb == null) {
425      return Future.error(EJDB2Error.invalidState());
426    }
427    final completer = Completer<String>();
428    final replyPort = RawReceivePort();
429    replyPort.handler = (dynamic reply) {
430      replyPort.close();
431      if (_checkCompleterPortError(completer, reply)) {
432        return;
433      }
434      completer.complete((reply as List).first as String);
435    };
436    _port().send([replyPort.sendPort, 'get', hdb as dynamic, collection, id]);
437    return completer.future;
438  }
439
440  /// Get json body of database metadata.
441  Future<String> info() {
442    final hdb = _get_handle();
443    if (hdb == null) {
444      return Future.error(EJDB2Error.invalidState());
445    }
446    final completer = Completer<String>();
447    final replyPort = RawReceivePort();
448    replyPort.handler = (dynamic reply) {
449      replyPort.close();
450      if (_checkCompleterPortError(completer, reply)) {
451        return;
452      }
453      completer.complete((reply as List).first as String);
454    };
455    _port().send([replyPort.sendPort, 'info', hdb as dynamic]);
456    return completer.future;
457  }
458
459  /// Remove document idenfied by [id] from [collection].
460  Future<void> del(String collection, int id) {
461    final hdb = _get_handle();
462    if (hdb == null) {
463      return Future.error(EJDB2Error.invalidState());
464    }
465    final completer = Completer<void>();
466    final replyPort = RawReceivePort();
467    replyPort.handler = (dynamic reply) {
468      replyPort.close();
469      if (_checkCompleterPortError(completer, reply)) {
470        return;
471      }
472      completer.complete();
473    };
474    _port().send([replyPort.sendPort, 'del', hdb as dynamic, collection, id]);
475    return completer.future;
476  }
477
478  /// Remove document idenfied by [id] from [collection].
479  /// Doesn't raise error if document is not found.
480  Future<void> delIgnoreNotFound(String collection, int id) =>
481      del(collection, id).catchError((err) {
482        if (err is EJDB2Error && err.notFound) {
483          return Future.value();
484        } else {
485          return Future.error(err as Object);
486        }
487      });
488
489  Future<void> renameCollection(String oldCollection, String newCollectionName) {
490    final hdb = _get_handle();
491    if (hdb == null) {
492      return Future.error(EJDB2Error.invalidState());
493    }
494    final completer = Completer<void>();
495    final replyPort = RawReceivePort();
496    replyPort.handler = (dynamic reply) {
497      replyPort.close();
498      if (_checkCompleterPortError(completer, reply)) {
499        return;
500      }
501      completer.complete();
502    };
503    _port().send([replyPort.sendPort, 'rename', hdb as dynamic, oldCollection, newCollectionName]);
504    return completer.future;
505  }
506
507  /// Ensures json document database index specified by [path] json pointer to string data type.
508  Future<void> ensureStringIndex(String collection, String path, {bool unique = false}) {
509    return _idx(collection, path, 0x04 | (unique ? 0x01 : 0));
510  }
511
512  /// Removes specified database index.
513  Future<void> removeStringIndex(String collection, String path, {bool unique = false}) {
514    return _rmi(collection, path, 0x04 | (unique ? 0x01 : 0));
515  }
516
517  /// Ensures json document database index specified by [path] json pointer to integer data type.
518  Future<void> ensureIntIndex(String collection, String path, {bool unique = false}) {
519    return _idx(collection, path, 0x08 | (unique ? 0x01 : 0));
520  }
521
522  /// Removes specified database index.
523  Future<void> removeIntIndex(String collection, String path, {bool unique = false}) {
524    return _rmi(collection, path, 0x08 | (unique ? 0x01 : 0));
525  }
526
527  /// Ensures json document database index specified by [path] json pointer to floating point data type.
528  Future<void> ensureFloatIndex(String collection, String path, {bool unique = false}) {
529    return _idx(collection, path, 0x10 | (unique ? 0x01 : 0));
530  }
531
532  /// Removes specified database index.
533  Future<void> removeFloatIndex(String collection, String path, {bool unique = false}) {
534    return _rmi(collection, path, 0x10 | (unique ? 0x01 : 0));
535  }
536
537  /// Removes database [collection].
538  Future<void> removeCollection(String collection) {
539    final hdb = _get_handle();
540    if (hdb == null) {
541      return Future.error(EJDB2Error.invalidState());
542    }
543    final completer = Completer<void>();
544    final replyPort = RawReceivePort();
545    replyPort.handler = (dynamic reply) {
546      replyPort.close();
547      if (_checkCompleterPortError(completer, reply)) {
548        return;
549      }
550      completer.complete();
551    };
552    _port().send([replyPort.sendPort, 'rmc', hdb as dynamic, collection]);
553    return completer.future;
554  }
555
556  /// Creates an online database backup image and copies it into the specified [fileName].
557  /// During online backup phase read/write database operations are allowed and not
558  /// blocked for significant amount of time. Returns future with backup
559  /// finish time as number of milliseconds since epoch.
560  Future<int> onlineBackup(String fileName) {
561    final hdb = _get_handle();
562    if (hdb == null) {
563      return Future.error(EJDB2Error.invalidState());
564    }
565    final completer = Completer<int>();
566    final replyPort = RawReceivePort();
567    replyPort.handler = (dynamic reply) {
568      replyPort.close();
569      if (_checkCompleterPortError(completer, reply)) {
570        return;
571      }
572      completer.complete((reply as List).first as int);
573    };
574    _port().send([replyPort.sendPort, 'bkp', hdb as dynamic, fileName]);
575    return completer.future;
576  }
577
578  /// Create instance of [query] specified for [collection].
579  /// If [collection] is not specified a [query] spec must contain collection name,
580  /// eg: `@mycollection/[foo=bar]`
581  JQL createQuery(String query, [String collection]) native 'create_query';
582
583  Future<void> _idx(String collection, String path, int mode) {
584    final hdb = _get_handle();
585    if (hdb == null) {
586      return Future.error(EJDB2Error.invalidState());
587    }
588    final completer = Completer<void>();
589    final replyPort = RawReceivePort();
590    replyPort.handler = (dynamic reply) {
591      replyPort.close();
592      if (_checkCompleterPortError(completer, reply)) {
593        return;
594      }
595      completer.complete();
596    };
597    _port().send([replyPort.sendPort, 'idx', hdb as dynamic, collection, path, mode]);
598    return completer.future;
599  }
600
601  Future<void> _rmi(String collection, String path, int mode) {
602    final hdb = _get_handle();
603    if (hdb == null) {
604      return Future.error(EJDB2Error.invalidState());
605    }
606    final completer = Completer<void>();
607    final replyPort = RawReceivePort();
608    replyPort.handler = (dynamic reply) {
609      replyPort.close();
610      if (_checkCompleterPortError(completer, reply)) {
611        return;
612      }
613      completer.complete();
614    };
615    _port().send([replyPort.sendPort, 'rmi', hdb as dynamic, collection, path, mode]);
616    return completer.future;
617  }
618
619  SendPort _port() native 'port';
620
621  void _set_handle(int? handle) native 'set_handle';
622
623  int? _get_handle() native 'get_handle';
624}
625
626String _asJsonString(dynamic val) {
627  if (val is String) {
628    return val;
629  } else {
630    return jsonEncode(val);
631  }
632}
633