├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── misc.xml └── modules.xml ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── analysis_options.yaml ├── example ├── isolate.dart └── main.dart ├── leveldb_dart.iml ├── lib ├── leveldb.cc ├── leveldb.dart └── libleveldb.so ├── pubspec.yaml └── test └── leveldb_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .packages 2 | *~ 3 | pubspec.lock 4 | .pub 5 | .idea/workspace.xml 6 | .idea/tasks.xml 7 | lib/*.o 8 | .idea/libraries/*.xml 9 | .idea/vcs.xml 10 | .idea/php.xml 11 | .idea/markdown-navigator.xml 12 | .idea/markdown-navigator/ 13 | .dart_tool/ 14 | .idea/inspectionProfiles/ 15 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | leveldb_dart -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Python 2.7.10 (/usr/bin/python2.7) 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | 3 | # By default the latest stable dart release is used. 4 | #dart: 5 | # Install the latest stable release 6 | #- stable 7 | 8 | # Run tests on precise and trusty. They have a different gcc abi. 9 | matrix: 10 | include: 11 | - os: linux 12 | dist: trusty 13 | - os: linux 14 | dist: precise 15 | - os: osx 16 | 17 | dart_task: 18 | - test: --platform vm 19 | - dartanalyzer: --fatal-warnings . 20 | - dartfmt 21 | 22 | 23 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Adam Lofts 2 | Simon Kuang -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 7.0.0 4 | 5 | Breaking changes: 6 | 7 | - Dart null safety 8 | - Use leveldb 1.2.3 9 | - OSX support dropped 10 | 11 | ## 6.0.2 12 | 13 | Non-breaking changes: 14 | 15 | - Link to stdc++. This seems to be required for Dart SDK >= 2.2.0 16 | 17 | ## 6.0.1 18 | 19 | Non-breaking changes: 20 | 21 | - Add OSX platform support 22 | 23 | ## 6.0.0 24 | 25 | Breaking changes: 26 | - Upgrade to dart2 27 | 28 | ## 5.0.1 29 | 30 | Non-breaking changes 31 | 32 | - Add @required annotation to keyEncoding and valueEncoding in LevelDB.open 33 | - Improve docs 34 | 35 | ## 5.0.0 36 | 37 | Breaking changes: 38 | 39 | - Remove `LevelEncoding` interface and use `dart:codec` directly. This better aligns the interface with 40 | the dart way of encoding and decoding and allows easily fusing new codecs. 41 | - Add new json.dart example to demonstrate encoding objects to the database (as JSON). 42 | 43 | ## 4.0.0 44 | 45 | Minor API update for [Sound Dart](https://www.dartlang.org/guides/language/sound-dart) 46 | 47 | Breaking changes: 48 | - The `keyEncoding` and `valueEncoding` parameters are now required when using the `LevelDB.open` function. 49 | When encoding utf8 keys and values `LevelDB.openUtf8` is the recommended constructor. 50 | 51 | ## 3.0.0 52 | 53 | Breaking changes: 54 | 55 | - Add generic parameters to `LevelDB` to improve type safety when using the API. 56 | Key/Value encoding parameters have been moved to the `LevelDB.open` function. 57 | - Minimum dart sdk version updated to `1.23.0` 58 | 59 | Non-breaking changes: 60 | 61 | - Upgrade to leveldb 1.20. This version is compatible with the previous on-disk format. See: https://github.com/google/leveldb/releases/tag/v1.20 62 | - Add `shared` parameter to `LevelDB.open`. This feature allows referencing 63 | the same underlying database from multiple isolates. 64 | - Add an example demonstrating how to use the `shared` parameter in muliple 65 | isolates. 66 | 67 | ## 2.0.3 68 | 69 | - Build leveldb with better compatibility. 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Adam Lofts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Users of this package should not need to run this makefile. 3 | # 4 | 5 | # First build a static leveldb lib. 6 | # The build script already does this but you need to add a couple of options to the static build. 7 | # Make the line look like: 8 | # OPT ?= -O2 -DNDEBUG -fPIC 9 | 10 | # The -fPIC enables linking the static lib into the object we will build. 11 | 12 | # Then set the source: 13 | 14 | LEVELDB_SOURCE=/home/adam/dev/fp3/dart/leveldb-1.23 15 | DART_SDK=/home/adam/dev/tools/dart-sdk-2.12.4 16 | 17 | LIBS=$(LEVELDB_SOURCE)/build/libleveldb.a 18 | # Select prod/debug args 19 | ARGS=-O2 -Wall 20 | # ARGS=-g -O0 -Wall -D_GLIBCXX_USE_CXX11_ABI=0 21 | 22 | UNAME_S := $(shell uname -s) 23 | 24 | ifeq ($(UNAME_S),Darwin) 25 | LIB_NAME = libleveldb.dylib 26 | ARGS_LINK = -dynamic -undefined dynamic_lookup 27 | endif 28 | ifeq ($(UNAME_S),Linux) 29 | LIB_NAME = libleveldb.so 30 | ARGS_LINK = -shared -Wl,-soname,$(LIB_NAME) 31 | endif 32 | 33 | all: lib/libleveldb.so 34 | 35 | lib/leveldb.o: lib/leveldb.cc 36 | g++ $(ARGS) -fPIC -I$(DART_SDK) -I$(LEVELDB_SOURCE)/include -DDART_SHARED_LIB -c lib/leveldb.cc -o lib/leveldb.o 37 | 38 | lib/libleveldb.so: lib/leveldb.o 39 | gcc $(ARGS) lib/leveldb.o $(ARGS_LINK) -o lib/$(LIB_NAME) $(LIBS) -lstdc++ 40 | 41 | clean: 42 | rm -f lib/*.o lib/*.so lib/*.dylib 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *Fast & simple storage - a Dart LevelDB wrapper* 2 | 3 | LevelDB Logo 4 | 5 | Build Status 6 | 7 | Introduction 8 | ------------ 9 | 10 | **[LevelDB](https://github.com/google/leveldb)** is a simple key/value data store built by Google, inspired by BigTable. It's used in Google 11 | Chrome and many other products. LevelDB supports arbitrary byte arrays as both keys and values, singular *get*, *put* and *delete* 12 | operations, *batched put and delete*, bi-directional iterators and simple compression using the very fast 13 | [Snappy](http://google.github.io/snappy/) algorithm. 14 | 15 | **leveldb_dart** aims to expose the features of LevelDB in a **Dart-friendly way**. 16 | 17 | LevelDB stores entries **sorted lexicographically by keys**. This makes [LevelDB.getItems](https://www.dartdocs.org/documentation/leveldb/latest/leveldb/LevelDB/getItems.html) a very powerful query mechanism. 18 | 19 | Platform Support 20 | ---------------- 21 | 22 | - [x] Modern 64-bit Linux platforms (e.g. Fedora 25, 26, Ubuntu 14.04, 15.10) 23 | - [x] Mac OS X 24 | 25 | Unsupported platforms: 26 | 27 | - [ ] Android (See [issue #12](https://github.com/adamlofts/leveldb_dart/issues/12)) 28 | - [ ] Windows (Not supported in base library [issue #466](https://github.com/google/leveldb/issues/466)) 29 | 30 | Basic usage 31 | ----------- 32 | 33 | Add `leveldb` to your `pubspec.yaml` file. 34 | 35 | ``` 36 | name: myproject 37 | dependencies: 38 | leveldb: 39 | ``` 40 | 41 | Open a database and read/write some keys and values.. 42 | 43 | ``` 44 | import 'dart:async'; 45 | import 'package:leveldb/leveldb.dart'; 46 | 47 | Future main() async { 48 | LevelDB db = await LevelDB.openUtf8("/tmp/testdb"); 49 | db.put("abc", "def"); 50 | String value = db.get("abc"); 51 | print("value is $value"); // value2 is def 52 | } 53 | ``` 54 | Check out [example/main.dart](https://github.com/adamlofts/leveldb_dart/blob/master/example/main.dart) to see how to read, write and iterate over keys and values. 55 | 56 | Documentation 57 | ------------- 58 | 59 | API Documentation is available at https://www.dartdocs.org/documentation/leveldb/latest/ 60 | 61 | Isolates (Threads) 62 | ------------------ 63 | 64 | *leveldb_dart* supports access to a database from multiple isolates by passing 65 | `shared: true` to the 66 | [LevelDB.open](https://www.dartdocs.org/documentation/leveldb/latest/leveldb/LevelDB/open.html) function. The `LevelDB` object 67 | returned by this function will share an underlying reference to the object in other isolates and changes will 68 | be visible between isolates. 69 | 70 | See [example/isolate.dart](https://github.com/adamlofts/leveldb_dart/blob/master/example/isolate.dart) for an example of using a database from multiple isolates (OS threads). 71 | 72 | 73 | Feature Support 74 | --------------- 75 | 76 | - [x] Read and write keys 77 | - [x] Forward iteration 78 | - [x] Multi-isolate 79 | - [ ] Backward iteration 80 | - [ ] Snapshots 81 | - [ ] Bulk get / put 82 | 83 | 84 | Custom Encoding and Decoding 85 | ---------------------------- 86 | 87 | By default you can use `LevelDB.openUtf8` to open a database with `String` keys and values which are encoded in UTF8. The `dart:codec` library 88 | can be used to create databases with custom encodings. See [example/json.dart](https://github.com/adamlofts/leveldb_dart/blob/master/example/json.dart) 89 | for an example which stores dart objects to the database via JSON encoding. 90 | 91 | 92 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: 3 | implicit-casts: false 4 | implicit-dynamic: false 5 | linter: 6 | rules: 7 | - always_declare_return_types 8 | - always_specify_types 9 | - annotate_overrides 10 | - avoid_as 11 | - avoid_empty_else 12 | - avoid_init_to_null 13 | - avoid_return_types_on_setters 14 | - await_only_futures 15 | - camel_case_types 16 | - cancel_subscriptions 17 | - close_sinks 18 | - comment_references 19 | - constant_identifier_names 20 | - control_flow_in_finally 21 | - empty_catches 22 | - empty_constructor_bodies 23 | - empty_statements 24 | - hash_and_equals 25 | - implementation_imports 26 | - iterable_contains_unrelated_type 27 | - library_names 28 | - library_prefixes 29 | - list_remove_unrelated_type 30 | - non_constant_identifier_names 31 | - one_member_abstracts 32 | - only_throw_errors 33 | - overridden_fields 34 | - package_api_docs 35 | - package_names 36 | - package_prefixed_library_names 37 | - prefer_is_not_empty 38 | - public_member_api_docs 39 | - slash_for_doc_comments 40 | - sort_unnamed_constructors_first 41 | - test_types_in_equals 42 | - throw_in_finally 43 | - type_annotate_public_apis 44 | - type_init_formals 45 | - unawaited_futures 46 | - unnecessary_getters_setters 47 | - unrelated_type_equality_checks 48 | - valid_regexps -------------------------------------------------------------------------------- /example/isolate.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:leveldb/leveldb.dart'; 5 | 6 | /// This example demonstrates how to access a database from multiple isolates. 7 | /// Isolates are implemented as os-threads in the dart vm so this allows you to 8 | /// use multiple cores. Access to the underlying level db from multiple isolates is safe. 9 | Future main() async { 10 | // Spawn some isolates. Each of these will write a key then read a key from the next thread. 11 | List runners = new Iterable.generate(5).map((int index) { 12 | return new Runner.spawn(index); 13 | }).toList(); 14 | 15 | await Future.wait(runners.map((Runner r) => r.finish)); 16 | } 17 | 18 | /// This method is called in different OS threads by the dart VM. 19 | Future run(int index) async { 20 | // Because shared: true is passed the DB returned by this method will reference the same 21 | // database. 22 | LevelDB db = 23 | await LevelDB.openUtf8("/tmp/testdb", shared: true); 24 | 25 | // Write our key to the db 26 | print("Thread $index write key $index -> $index"); 27 | db.put("$index", "$index"); 28 | 29 | // Sleep 1 second 30 | await new Future.delayed(const Duration(seconds: 1)); 31 | 32 | // Now read the key from the next thread 33 | String nextKey = "${(index + 1) % 5}"; 34 | print("Thread $index read key $nextKey -> ${db.get(nextKey)}"); 35 | } 36 | 37 | /// Helper class to run an isolate and wait for it to finish. 38 | class Runner { 39 | final Completer _finish = new Completer(); 40 | final RawReceivePort _finishPort = new RawReceivePort(); 41 | 42 | /// Run an isolate. 43 | Runner.spawn(int index) { 44 | _finishPort.handler = (dynamic _) { 45 | _finish.complete(); 46 | _finishPort.close(); 47 | }; 48 | Isolate.spawn(run, index, onExit: _finishPort.sendPort); 49 | } 50 | 51 | /// Future completed when the isolate exits normally. 52 | Future get finish => _finish.future; 53 | } 54 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:leveldb/leveldb.dart'; 5 | 6 | /// main example. 7 | Future main() async { 8 | // Open a database. It is created if it does not already exist. Only one process can 9 | // open a database at a time. 10 | LevelDB db = await LevelDB.openUtf8("/tmp/testdb"); 11 | 12 | // By default keys and values are strings. 13 | db.put("abc", "def"); 14 | 15 | // Now get the key 16 | String? value = db.get("abc"); 17 | print("value is $value"); // value2 is def 18 | 19 | // Delete the key 20 | db.delete("abc"); 21 | 22 | // If a key does not exist we get null back 23 | String? value3 = db.get("abc"); 24 | print("value3 is $value3"); // value3 is null 25 | 26 | // Now lets add a few key-value pairs 27 | for (int i in new Iterable.generate(5)) { 28 | db.put("key-$i", "value-$i"); 29 | } 30 | 31 | // Iterate through the key-value pairs in key order. 32 | for (LevelItem v in db.getItems()) { 33 | print( 34 | "Row: ${v.key} ${v.value}"); // prints Row: key-0 value-0, Row: key-1 value-1, ... 35 | } 36 | 37 | // Iterate keys between key-1 and key-3 38 | for (LevelItem v in db.getItems(gte: "key-1", lte: "key-3")) { 39 | print( 40 | "Row: ${v.key} ${v.value}"); // prints Row: key-1 value-1, Row: key-2 value-2, Row: key-3 value-3 41 | } 42 | 43 | // Iterate explicitly. This avoids allocation of LevelItem objects if you never call it.current. 44 | LevelIterator it = db.getItems(limit: 1).iterator; 45 | while (it.moveNext()) { 46 | print("${it.currentKey} ${it.currentValue}"); 47 | } 48 | 49 | // Just key iteration 50 | for (dynamic key in db.getItems().keys) { 51 | print("Key $key"); // Prints Key key-0, Key key-1, ... 52 | } 53 | 54 | // Value iteration 55 | for (dynamic value in db.getItems().values) { 56 | print("Value $value"); // Prints Key value-0, Key value-1, ... 57 | } 58 | 59 | // Close the db. This free's all resources associated with the db. 60 | // All iterators will throw if used after this call. 61 | db.close(); 62 | 63 | // Open a new db which will use raw UInt8List data. This is faster since it avoids any decoding. 64 | LevelDB db2 = 65 | await LevelDB.openUint8List("/tmp/testdb"); 66 | 67 | for (LevelItem item in db2.getItems()) { 68 | print("${item.key}"); // Prints [107, 101, 121, 45, 48], ... 69 | } 70 | 71 | db2.close(); 72 | } 73 | -------------------------------------------------------------------------------- /leveldb_dart.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/leveldb.cc: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "include/dart_api.h" 13 | #include "include/dart_native_api.h" 14 | 15 | #include "leveldb/db.h" 16 | #include "leveldb/filter_policy.h" 17 | 18 | 19 | const int BLOOM_BITS_PER_KEY = 10; 20 | 21 | 22 | Dart_NativeFunction ResolveName(Dart_Handle name, 23 | int argc, 24 | bool* auto_setup_scope); 25 | 26 | 27 | DART_EXPORT Dart_Handle leveldb_Init(Dart_Handle parent_library) { 28 | if (Dart_IsError(parent_library)) { 29 | return parent_library; 30 | } 31 | 32 | Dart_Handle result_code = 33 | Dart_SetNativeResolver(parent_library, ResolveName, NULL); 34 | if (Dart_IsError(result_code)) { 35 | return result_code; 36 | } 37 | 38 | return Dart_Null(); 39 | } 40 | 41 | 42 | int64_t statusToError(leveldb::Status status) { 43 | if (status.IsNotFound()) { 44 | return -5; 45 | } 46 | if (status.IsIOError()) { 47 | return -2; 48 | } 49 | if (status.IsCorruption()) { 50 | return -3; 51 | } 52 | // LevelDB does not provide Status::IsInvalidArgument so we just assume all other errors are invalid argument. 53 | if (!status.ok()) { 54 | return -4; 55 | } 56 | return 0; 57 | } 58 | 59 | 60 | struct DB { 61 | leveldb::DB *db; 62 | int64_t refcount; 63 | 64 | bool is_shared; 65 | char* path; 66 | int64_t block_size; 67 | bool create_if_missing; 68 | bool error_if_exists; 69 | 70 | pthread_t thread; 71 | std::deque notify_list; 72 | int64_t open_status; 73 | pthread_mutex_t mutex; 74 | }; 75 | 76 | 77 | struct cmp_str { 78 | bool operator()(char const *a, char const *b) const { 79 | return std::strcmp(a, b) < 0; 80 | } 81 | }; 82 | 83 | 84 | typedef std::map DBMap; 85 | pthread_mutex_t shared_mutex = PTHREAD_MUTEX_INITIALIZER; 86 | DBMap sharedDBs; 87 | 88 | 89 | void* runOpen(void* ptr) { 90 | // This function may not take the shared mutex because we take it when joining to this thread. 91 | DB *native_db = (DB*) ptr; 92 | leveldb::Options options; 93 | options.create_if_missing = native_db->create_if_missing; 94 | options.error_if_exists = native_db->error_if_exists; 95 | options.block_size = native_db->block_size; 96 | options.filter_policy = leveldb::NewBloomFilterPolicy(BLOOM_BITS_PER_KEY); 97 | 98 | leveldb::Status status = leveldb::DB::Open(options, native_db->path, &native_db->db); 99 | 100 | // Notify all ports the new status. 101 | pthread_mutex_lock(&native_db->mutex); 102 | native_db->open_status = statusToError(status); 103 | 104 | while (!native_db->notify_list.empty()) { 105 | Dart_Port port = native_db->notify_list.front(); 106 | native_db->notify_list.pop_front(); 107 | Dart_PostInteger(port, native_db->open_status); 108 | } 109 | pthread_mutex_unlock(&native_db->mutex); 110 | return NULL; 111 | } 112 | 113 | /// Open a db and take a reference to it. 114 | /// open_port_id will be notified when the db is ready or an error occurs. 115 | DB* referenceDB(const char *path, bool is_shared, Dart_Port open_port_id, bool create_if_missing, bool error_if_exists, int64_t block_size) { 116 | DB* db = NULL; 117 | bool is_new = false; 118 | 119 | pthread_mutex_lock(&shared_mutex); 120 | 121 | // Look for the db by path 122 | if (is_shared) { 123 | DBMap::iterator it = sharedDBs.find(path); 124 | if (it != sharedDBs.end()) { 125 | db = it->second; 126 | assert(db->refcount > 0); 127 | } 128 | } 129 | 130 | // Create db if not found 131 | if (db == NULL) { 132 | is_new = true; 133 | db = new DB(); 134 | db->is_shared = is_shared; 135 | db->path = strdup(path); 136 | db->refcount = 0; 137 | db->open_status = 1; 138 | db->create_if_missing = create_if_missing; 139 | db->error_if_exists = error_if_exists; 140 | db->block_size = block_size; 141 | pthread_mutex_init(&db->mutex, NULL); 142 | } 143 | 144 | // If the db is shared add it to the map 145 | if (is_shared) { 146 | sharedDBs[db->path] = db; 147 | } 148 | 149 | // If the db is open then just post a reply now. Otherwise add the port to the notify list. 150 | pthread_mutex_lock(&db->mutex); 151 | db->refcount += 1; 152 | if (db->open_status <= 0) { 153 | // The open thread has finished. 154 | Dart_PostInteger(open_port_id, db->open_status); 155 | } else { 156 | db->notify_list.push_back(open_port_id); 157 | } 158 | pthread_mutex_unlock(&db->mutex); 159 | pthread_mutex_unlock(&shared_mutex); 160 | 161 | // Spawn a thread to open the DB 162 | if (is_new) { 163 | pthread_attr_t attr; 164 | pthread_attr_init(&attr); 165 | pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); 166 | int rc = pthread_create(&db->thread, &attr, runOpen, (void*)db); 167 | assert(rc == 0); 168 | pthread_attr_destroy(&attr); 169 | } 170 | return db; 171 | } 172 | 173 | 174 | /// Drop a reference to a db. 175 | /// May result in the db being closed. 176 | void unreferenceDB(DB* db) { 177 | bool is_finished; 178 | // Take the shared mutex and the db mutex. This is so that if the refcount drops to 0 we can safely remove it 179 | // from the shared map. 180 | pthread_mutex_lock(&shared_mutex); 181 | pthread_mutex_lock(&db->mutex); 182 | db->refcount -= 1; 183 | is_finished = db->refcount == 0; 184 | 185 | // If the db is shared then remove it from the map. 186 | if (is_finished && db->is_shared) { 187 | sharedDBs.erase(db->path); 188 | } 189 | 190 | pthread_mutex_unlock(&db->mutex); 191 | 192 | if (is_finished) { 193 | // It is possible that unreferenceDB is called before db->thread is initialized if a 2nd thread quickly takes a reference to a 194 | // shared db and then drops it. However the initializing thread still has a reference so it is safe to call pthread_join() 195 | // if the refcount was 0 196 | pthread_join(db->thread, NULL); 197 | 198 | // The actual closing of the db and its file descriptors must be run whilst 199 | // the shared lock is taken so that any threads attempting to open the same file will 200 | // succeed. 201 | delete db->path; 202 | delete db->db; 203 | delete db; 204 | } 205 | 206 | pthread_mutex_unlock(&shared_mutex); 207 | } 208 | 209 | 210 | struct NativeIterator; 211 | 212 | 213 | struct NativeDB { 214 | // Reference to the DB. NULL if closed. 215 | DB* db; 216 | std::list *iterators; 217 | }; 218 | 219 | 220 | struct NativeIterator { 221 | NativeDB *native_db; 222 | 223 | leveldb::Iterator *iterator; 224 | bool is_finalized; 225 | 226 | // Iterator params 227 | int64_t limit; 228 | bool is_gt_closed; 229 | bool is_lt_closed; 230 | uint8_t* gt; 231 | int64_t gt_len; 232 | uint8_t* lt; 233 | int64_t lt_len; 234 | bool is_fill_cache; 235 | 236 | // Iterator state 237 | int64_t count; 238 | }; 239 | 240 | 241 | /** 242 | * Finalize the iterator. 243 | */ 244 | static void iteratorFinalize(NativeIterator *it_ref) { 245 | if (it_ref->is_finalized) { 246 | return; 247 | } 248 | it_ref->is_finalized = true; 249 | 250 | // This iterator will only be in the db list if the level db iterator has been created (i.e. the stream has 251 | // started). 252 | if (it_ref->iterator != NULL) { 253 | // Remove the iterator from the db list 254 | it_ref->native_db->iterators->remove(it_ref); 255 | delete it_ref->iterator; 256 | it_ref->iterator = NULL; 257 | } 258 | 259 | delete it_ref->gt; 260 | delete it_ref->lt; 261 | } 262 | 263 | 264 | /** 265 | * Finalizer called when the dart LevelDB instance is not reachable. 266 | * */ 267 | static void NativeDBFinalizer(void* isolate_callback_data, void* peer) { 268 | NativeDB* native_db = (NativeDB*) peer; 269 | 270 | // If the db reference is not NULL then the user did not call close on the db before it went out of scope. 271 | // We unreference it now. 272 | if (native_db->db != NULL) { 273 | unreferenceDB(native_db->db); 274 | native_db->db = NULL; 275 | } 276 | 277 | // Finalize every iterator. The iterators remove themselves from the array. 278 | while (!native_db->iterators->empty()) { 279 | iteratorFinalize(native_db->iterators->front()); 280 | } 281 | delete native_db->iterators; 282 | 283 | delete native_db; 284 | } 285 | 286 | 287 | Dart_Handle HandleError(Dart_Handle handle) { 288 | if (Dart_IsError(handle)) { 289 | Dart_PropagateError(handle); 290 | } 291 | return handle; 292 | } 293 | 294 | 295 | /** 296 | * Finalizer called when the dart instance is not reachable. 297 | * */ 298 | static void NativeIteratorFinalizer(void* isolate_callback_data, void* peer) { 299 | NativeIterator* it_ref = (NativeIterator*) peer; 300 | iteratorFinalize(it_ref); 301 | delete it_ref; 302 | } 303 | 304 | 305 | void dbOpen(Dart_NativeArguments arguments) { // (bool shared, SendPort port, String path, int blockSize, bool create_if_missing, bool error_if_exists) 306 | Dart_EnterScope(); 307 | 308 | NativeDB* native_db = new NativeDB(); 309 | 310 | const char* path; 311 | Dart_Handle arg2 = Dart_GetNativeArgument(arguments, 3); 312 | Dart_StringToCString(arg2, &path); 313 | 314 | Dart_Port port_id; 315 | Dart_Handle arg1 = Dart_GetNativeArgument(arguments, 2); 316 | Dart_SendPortGetId(arg1, &port_id); 317 | 318 | bool is_shared; 319 | bool create_if_missing; 320 | bool error_if_exists; 321 | int64_t block_size; 322 | 323 | Dart_GetNativeBooleanArgument(arguments, 1, &is_shared); 324 | Dart_GetNativeIntegerArgument(arguments, 4, &block_size); 325 | Dart_GetNativeBooleanArgument(arguments, 5, &create_if_missing); 326 | Dart_GetNativeBooleanArgument(arguments, 6, &error_if_exists); 327 | 328 | native_db->db = referenceDB(path, is_shared, port_id, create_if_missing, error_if_exists, 1024); 329 | native_db->iterators = new std::list(); 330 | 331 | Dart_Handle arg0 = Dart_GetNativeArgument(arguments, 0); 332 | Dart_SetNativeInstanceField(arg0, 0, (intptr_t) native_db); 333 | 334 | Dart_NewWeakPersistentHandle(arg0, (void*) native_db, sizeof(NativeDB) /* external_allocation_size */, NativeDBFinalizer); 335 | 336 | Dart_SetReturnValue(arguments, Dart_Null()); 337 | Dart_ExitScope(); 338 | } 339 | 340 | 341 | // SYNC API 342 | 343 | 344 | // Throw a LevelClosedError. This function does not return. 345 | void throwClosedException() { 346 | Dart_Handle klass = Dart_GetNonNullableType(Dart_LookupLibrary(Dart_NewStringFromCString("package:leveldb/leveldb.dart")), Dart_NewStringFromCString("LevelClosedError"), 0, NULL); 347 | Dart_Handle exception = Dart_New(klass, Dart_NewStringFromCString("_internal"), 0, NULL); 348 | Dart_ThrowException(exception); 349 | } 350 | 351 | 352 | // If status is not ok then throw an error. This function does not return. 353 | void maybeThrowStatus(leveldb::Status status) { 354 | if (status.ok()) { 355 | return; 356 | } 357 | Dart_Handle library = Dart_LookupLibrary(Dart_NewStringFromCString("package:leveldb/leveldb.dart")); 358 | Dart_Handle klass; 359 | if (status.IsCorruption()) { 360 | klass = Dart_GetNonNullableType(library, Dart_NewStringFromCString("LevelCorruptionError"), 0, NULL); 361 | } else { 362 | klass = Dart_GetNonNullableType(library, Dart_NewStringFromCString("LevelIOError"), 0, NULL); 363 | } 364 | Dart_Handle exception = Dart_New(klass, Dart_NewStringFromCString("_internal"), 0, NULL); 365 | Dart_ThrowException(exception); 366 | } 367 | 368 | 369 | void syncNew(Dart_NativeArguments arguments) { // (this, db, limit, fillCache, gt, is_gt_closed, lt, is_lt_closed) 370 | Dart_EnterScope(); 371 | 372 | NativeDB *native_db; 373 | Dart_Handle arg1 = Dart_GetNativeArgument(arguments, 1); 374 | Dart_GetNativeInstanceField(arg1, 0, (intptr_t*) &native_db); 375 | 376 | if (native_db->db == NULL) { 377 | throwClosedException(); 378 | assert(false); // Not reached 379 | } 380 | 381 | NativeIterator* it_ref = new NativeIterator(); 382 | it_ref->native_db = native_db; 383 | it_ref->is_finalized = false; 384 | it_ref->iterator = NULL; 385 | it_ref->count = 0; 386 | 387 | Dart_Handle arg0 = Dart_GetNativeArgument(arguments, 0); 388 | Dart_SetNativeInstanceField(arg0, 0, (intptr_t) it_ref); 389 | 390 | Dart_GetNativeIntegerArgument(arguments, 2, &it_ref->limit); 391 | Dart_GetNativeBooleanArgument(arguments, 3, &it_ref->is_fill_cache); 392 | 393 | Dart_Handle arg5 = Dart_GetNativeArgument(arguments, 4); 394 | if (Dart_IsNull(arg5)) { 395 | it_ref->gt = NULL; 396 | it_ref->gt_len = 0; 397 | } else { 398 | Dart_TypedData_Type typed_data_type = Dart_GetTypeOfTypedData(arg5); 399 | assert(typed_data_type == Dart_TypedData_kUint8); 400 | 401 | char *data; 402 | intptr_t len; 403 | Dart_TypedDataAcquireData(arg5, &typed_data_type, (void**)&data, &len); 404 | it_ref->gt_len = len; 405 | it_ref->gt = (uint8_t*) malloc(len); 406 | memcpy(it_ref->gt, data, len); 407 | Dart_TypedDataReleaseData(arg5); 408 | } 409 | 410 | Dart_Handle arg6 = Dart_GetNativeArgument(arguments, 6); 411 | if (Dart_IsNull(arg6)) { 412 | it_ref->lt = NULL; 413 | it_ref->lt_len = 0; 414 | } else { 415 | Dart_TypedData_Type typed_data_type = Dart_GetTypeOfTypedData(arg6); 416 | assert(typed_data_type != Dart_TypedData_kInvalid); 417 | 418 | char *data; 419 | intptr_t len; 420 | Dart_TypedDataAcquireData(arg6, &typed_data_type, (void**)&data, &len); 421 | it_ref->lt_len = len; 422 | it_ref->lt = (uint8_t*) malloc(len); 423 | memcpy(it_ref->lt, data, len); 424 | Dart_TypedDataReleaseData(arg6); 425 | } 426 | 427 | Dart_GetNativeBooleanArgument(arguments, 5, &it_ref->is_gt_closed); 428 | Dart_GetNativeBooleanArgument(arguments, 7, &it_ref->is_lt_closed); 429 | 430 | // We just pass the directly allocated size of the iterator here. The iterator holds a lot of other data in 431 | // memory when it mmaps the files but I'm not sure how to account for it. 432 | // Because the GC is not seeing all of the allocated memory it is important to manually call finalize() on the 433 | // iterator when we are done with it (for example when the iterator reaches the end of its range). 434 | Dart_NewWeakPersistentHandle(arg0, (void*) it_ref, /* external_allocation_size */ sizeof(NativeIterator), NativeIteratorFinalizer); 435 | 436 | Dart_SetReturnValue(arguments, Dart_Null()); 437 | Dart_ExitScope(); 438 | } 439 | 440 | 441 | // http://stackoverflow.com/questions/2022179/c-quick-calculation-of-next-multiple-of-4 442 | uint32_t increaseToMultipleOf4(uint32_t v) { 443 | return (v + 3) & ~0x03; 444 | } 445 | 446 | 447 | void syncNext(Dart_NativeArguments arguments) { // (this) 448 | Dart_EnterScope(); 449 | 450 | NativeIterator *native_iterator; 451 | Dart_Handle arg0 = Dart_GetNativeArgument(arguments, 0); 452 | Dart_GetNativeInstanceField(arg0, 0, (intptr_t*) &native_iterator); 453 | 454 | NativeDB *native_db = native_iterator->native_db; 455 | leveldb::Iterator* it = native_iterator->iterator; 456 | 457 | if (native_db->db == NULL) { 458 | throwClosedException(); 459 | assert(false); // Not reached 460 | } 461 | 462 | // If it is NULL we need to create the iterator and perform the initial seek. 463 | if (!native_iterator->is_finalized && it == NULL) { 464 | leveldb::ReadOptions options; 465 | options.fill_cache = native_iterator->is_fill_cache; 466 | it = native_db->db->db->NewIterator(options); 467 | 468 | native_iterator->iterator = it; 469 | // Add the iterator to the db list. This is so we know to finalize it before finalizing the db. 470 | native_db->iterators->push_back(native_iterator); 471 | 472 | if (native_iterator->gt_len > 0) { 473 | leveldb::Slice start_slice = leveldb::Slice((char*)native_iterator->gt, native_iterator->gt_len); 474 | it->Seek(start_slice); 475 | 476 | if (!native_iterator->is_gt_closed && it->Valid()) { 477 | // If we are pointing at start_slice and not inclusive then we need to advance by 1 478 | leveldb::Slice key = it->key(); 479 | if (key.compare(start_slice) == 0) { 480 | it->Next(); 481 | } 482 | } 483 | } else { 484 | it->SeekToFirst(); 485 | } 486 | } 487 | 488 | leveldb::Slice end_slice = leveldb::Slice((char*)native_iterator->lt, native_iterator->lt_len); 489 | bool is_valid = false; 490 | bool is_limit_reached = native_iterator->limit >= 0 && native_iterator->count >= native_iterator->limit; 491 | bool is_query_limit_reached = false; 492 | 493 | leveldb::Slice key; 494 | leveldb::Slice value; 495 | if (!native_iterator->is_finalized) { 496 | is_valid = it->Valid(); 497 | } 498 | 499 | if (is_valid) { 500 | key = it->key(); 501 | value = it->value(); 502 | 503 | // Check if key is equal to end slice 504 | if (native_iterator->lt_len > 0) { 505 | int cmp = key.compare(end_slice); 506 | if (cmp == 0 && !native_iterator->is_lt_closed) { // key == end_slice and not closed 507 | is_query_limit_reached = true; 508 | } 509 | if (cmp > 0) { // key > end_slice 510 | is_query_limit_reached = true; 511 | } 512 | } 513 | } 514 | 515 | Dart_Handle result = Dart_Null(); 516 | 517 | if (!is_valid || is_query_limit_reached || is_limit_reached) { 518 | // Iteration is finished. Any subsequent calls to syncNext() will return null so we can finalize the iterator 519 | // here. 520 | iteratorFinalize(native_iterator); 521 | } else { 522 | // Copy key and value into same buffer. 523 | // Align the value array to a multiple of 4 bytes so the offset of the view in dart is a multiple of 4. 524 | uint32_t key_size_mult_4 = increaseToMultipleOf4(key.size()); 525 | result = Dart_NewTypedData(Dart_TypedData_kUint8, key_size_mult_4 + value.size() + 4); 526 | uint8_t *data; 527 | intptr_t len; 528 | Dart_TypedData_Type t; 529 | Dart_TypedDataAcquireData(result, &t, (void**)&data, &len); 530 | data[0] = key.size() & 0xFF; 531 | data[1] = (key.size() >> 8) & 0xFF; 532 | data[2] = key_size_mult_4 & 0xFF; 533 | data[3] = (key_size_mult_4 >> 8) & 0xFF; 534 | memcpy(data + 4, key.data(), key.size()); 535 | memcpy(data + 4 + key_size_mult_4, value.data(), value.size()); 536 | Dart_TypedDataReleaseData(result); 537 | 538 | native_iterator->count += 1; 539 | it->Next(); 540 | } 541 | 542 | Dart_SetReturnValue(arguments, result); 543 | Dart_ExitScope(); 544 | } 545 | 546 | 547 | void syncGet(Dart_NativeArguments arguments) { // (this, key) 548 | Dart_EnterScope(); 549 | 550 | NativeDB *native_db; 551 | Dart_Handle arg0 = Dart_GetNativeArgument(arguments, 0); 552 | Dart_GetNativeInstanceField(arg0, 0, (intptr_t*) &native_db); 553 | 554 | if (native_db->db == NULL) { 555 | throwClosedException(); 556 | assert(false); // Not reached 557 | } 558 | 559 | Dart_Handle arg1 = Dart_GetNativeArgument(arguments, 1); 560 | Dart_TypedData_Type typed_data_type = Dart_GetTypeOfTypedData(arg1); 561 | assert(typed_data_type == Dart_TypedData_kUint8); 562 | 563 | char *data; 564 | intptr_t len; 565 | Dart_TypedDataAcquireData(arg1, &typed_data_type, (void**)&data, &len); 566 | 567 | leveldb::Slice key = leveldb::Slice(data, len); 568 | 569 | std::string value; 570 | leveldb::Status status = native_db->db->db->Get(leveldb::ReadOptions(), key, &value); 571 | Dart_TypedDataReleaseData(arg1); 572 | 573 | Dart_Handle result; 574 | if (status.IsNotFound()) { 575 | result = Dart_Null(); 576 | } else if (status.ok()) { 577 | result = Dart_NewTypedData(Dart_TypedData_kUint8, value.size()); 578 | Dart_TypedData_Type t; 579 | Dart_TypedDataAcquireData(result, &t, (void**)&data, &len); 580 | memcpy(data, value.data(), value.size()); 581 | Dart_TypedDataReleaseData(result); 582 | } else { 583 | maybeThrowStatus(status); 584 | assert(false); // Not reached 585 | } 586 | 587 | Dart_SetReturnValue(arguments, result); 588 | Dart_ExitScope(); 589 | } 590 | 591 | 592 | void syncPut(Dart_NativeArguments arguments) { // (this, key, value, sync) 593 | Dart_EnterScope(); 594 | 595 | NativeDB *native_db; 596 | Dart_Handle arg0 = Dart_GetNativeArgument(arguments, 0); 597 | Dart_GetNativeInstanceField(arg0, 0, (intptr_t*) &native_db); 598 | 599 | if (native_db->db == NULL) { 600 | throwClosedException(); 601 | assert(false); // Not reached 602 | } 603 | 604 | Dart_Handle arg1 = Dart_GetNativeArgument(arguments, 1); 605 | Dart_TypedData_Type typed_data_type1; 606 | 607 | Dart_Handle arg2 = Dart_GetNativeArgument(arguments, 2); 608 | Dart_TypedData_Type typed_data_type2; 609 | 610 | bool is_sync; 611 | Dart_GetNativeBooleanArgument(arguments, 3, &is_sync); 612 | 613 | char *data1, *data2; 614 | intptr_t len1, len2; 615 | Dart_TypedDataAcquireData(arg1, &typed_data_type1, (void**)&data1, &len1); 616 | Dart_TypedDataAcquireData(arg2, &typed_data_type2, (void**)&data2, &len2); 617 | 618 | assert(typed_data_type1 == Dart_TypedData_kUint8); 619 | assert(typed_data_type2 == Dart_TypedData_kUint8); 620 | 621 | leveldb::Slice key = leveldb::Slice(data1, len1); 622 | leveldb::Slice value = leveldb::Slice(data2, len2); 623 | 624 | leveldb::WriteOptions options; 625 | options.sync = is_sync; 626 | 627 | leveldb::Status status = native_db->db->db->Put(options, key, value); 628 | 629 | Dart_TypedDataReleaseData(arg1); 630 | Dart_TypedDataReleaseData(arg2); 631 | 632 | maybeThrowStatus(status); 633 | 634 | Dart_SetReturnValue(arguments, Dart_Null()); 635 | Dart_ExitScope(); 636 | } 637 | 638 | 639 | void syncDelete(Dart_NativeArguments arguments) { // (this, key) 640 | Dart_EnterScope(); 641 | 642 | NativeDB *native_db; 643 | Dart_Handle arg0 = Dart_GetNativeArgument(arguments, 0); 644 | Dart_GetNativeInstanceField(arg0, 0, (intptr_t*) &native_db); 645 | 646 | if (native_db->db == NULL) { 647 | throwClosedException(); 648 | assert(false); // Not reached 649 | } 650 | 651 | Dart_Handle arg1 = Dart_GetNativeArgument(arguments, 1); 652 | Dart_TypedData_Type typed_data_type = Dart_GetTypeOfTypedData(arg1); 653 | assert(typed_data_type == Dart_TypedData_kUint8); 654 | 655 | char *data; 656 | intptr_t len; 657 | Dart_TypedDataAcquireData(arg1, &typed_data_type, (void**)&data, &len); 658 | 659 | leveldb::Slice key = leveldb::Slice(data, len); 660 | leveldb::Status status = native_db->db->db->Delete(leveldb::WriteOptions(), key); 661 | Dart_TypedDataReleaseData(arg1); 662 | 663 | maybeThrowStatus(status); 664 | 665 | Dart_SetReturnValue(arguments, Dart_Null()); 666 | Dart_ExitScope(); 667 | } 668 | 669 | 670 | void syncClose(Dart_NativeArguments arguments) { // (this) 671 | Dart_EnterScope(); 672 | 673 | NativeDB *native_db; 674 | Dart_Handle arg0 = Dart_GetNativeArgument(arguments, 0); 675 | Dart_GetNativeInstanceField(arg0, 0, (intptr_t*) &native_db); 676 | 677 | if (native_db->db == NULL) { 678 | // DB has already been closed 679 | throwClosedException(); 680 | assert(false); // Not reached 681 | } 682 | 683 | // Finalize all iterators 684 | while (!native_db->iterators->empty()) { 685 | iteratorFinalize(native_db->iterators->front()); 686 | } 687 | 688 | unreferenceDB(native_db->db); 689 | native_db->db = NULL; 690 | 691 | Dart_SetReturnValue(arguments, Dart_Null()); 692 | Dart_ExitScope(); 693 | } 694 | 695 | 696 | // Plugin 697 | 698 | struct FunctionLookup { 699 | const char* name; 700 | Dart_NativeFunction function; 701 | }; 702 | 703 | 704 | FunctionLookup function_list[] = { 705 | {"DB_Open", dbOpen}, 706 | 707 | {"SyncIterator_New", syncNew}, 708 | {"SyncIterator_Next", syncNext}, 709 | 710 | {"SyncGet", syncGet}, 711 | {"SyncPut", syncPut}, 712 | {"SyncDelete", syncDelete}, 713 | {"SyncClose", syncClose}, 714 | 715 | {NULL, NULL}}; 716 | 717 | 718 | FunctionLookup no_scope_function_list[] = { 719 | {NULL, NULL} 720 | }; 721 | 722 | 723 | Dart_NativeFunction ResolveName(Dart_Handle name, 724 | int argc, 725 | bool* auto_setup_scope) { 726 | if (!Dart_IsString(name)) { 727 | return NULL; 728 | } 729 | Dart_NativeFunction result = NULL; 730 | if (auto_setup_scope == NULL) { 731 | return NULL; 732 | } 733 | Dart_EnterScope(); 734 | const char* cname; 735 | HandleError(Dart_StringToCString(name, &cname)); 736 | 737 | for (int i=0; function_list[i].name != NULL; ++i) { 738 | if (strcmp(function_list[i].name, cname) == 0) { 739 | *auto_setup_scope = true; 740 | result = function_list[i].function; 741 | break; 742 | } 743 | } 744 | 745 | if (result != NULL) { 746 | Dart_ExitScope(); 747 | return result; 748 | } 749 | 750 | for (int i=0; no_scope_function_list[i].name != NULL; ++i) { 751 | if (strcmp(no_scope_function_list[i].name, cname) == 0) { 752 | *auto_setup_scope = false; 753 | result = no_scope_function_list[i].function; 754 | break; 755 | } 756 | } 757 | 758 | Dart_ExitScope(); 759 | return result; 760 | } 761 | -------------------------------------------------------------------------------- /lib/leveldb.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Adam Lofts 2 | 3 | library leveldb; 4 | 5 | import 'dart:convert' as convert; 6 | import 'dart:async' show Future, Completer; 7 | import 'dart:isolate' show RawReceivePort, SendPort; 8 | import 'dart:typed_data' show Uint8List; 9 | import 'dart:nativewrappers' show NativeFieldWrapperClass2; 10 | import 'dart:collection' show IterableBase; 11 | 12 | import 'dart-ext:leveldb'; 13 | 14 | /// Base class for all exceptions thrown by leveldb_dart. 15 | abstract class LevelError implements Exception { 16 | final String _msg; 17 | const LevelError._internal(this._msg); 18 | @override 19 | String toString() => 'LevelError: $_msg'; 20 | } 21 | 22 | /// Exception thrown if the database is used after it has been closed. 23 | class LevelClosedError extends LevelError { 24 | const LevelClosedError._internal() : super._internal("DB already closed"); 25 | } 26 | 27 | /// Exception thrown if a general IO error is encountered. 28 | class LevelIOError extends LevelError { 29 | const LevelIOError._internal() : super._internal("IOError"); 30 | } 31 | 32 | /// Exception thrown if the db is corrupted 33 | class LevelCorruptionError extends LevelError { 34 | const LevelCorruptionError._internal() : super._internal("Corruption error"); 35 | } 36 | 37 | /// Exception thrown if invalid argument (e.g. if the database does not exist and createIfMissing is false) 38 | class LevelInvalidArgumentError extends LevelError { 39 | const LevelInvalidArgumentError._internal() 40 | : super._internal("Invalid argument"); 41 | } 42 | 43 | /// Exception thrown if `LevelIterator.current` used outside of valid range. 44 | class LevelInvalidIterator extends LevelError { 45 | const LevelInvalidIterator._internal() 46 | : super._internal("Iterator used before or after range"); 47 | } 48 | 49 | class _Uint8ListEncoder extends convert.Converter, Uint8List> { 50 | const _Uint8ListEncoder(); 51 | @override 52 | Uint8List convert(List input) => new Uint8List.fromList(input); 53 | } 54 | 55 | class _Uint8ListDecoder extends convert.Converter> { 56 | const _Uint8ListDecoder(); 57 | @override 58 | List convert(Uint8List input) => input; 59 | } 60 | 61 | /// This codec will encode a [List] to the [Uint8List] required by LevelDB dart. 62 | class Uint8ListCodec extends convert.Codec, Uint8List> { 63 | /// Default constructor 64 | const Uint8ListCodec(); 65 | @override 66 | convert.Converter, Uint8List> get encoder => 67 | const _Uint8ListEncoder(); 68 | @override 69 | convert.Converter> get decoder => 70 | const _Uint8ListDecoder(); 71 | } 72 | 73 | class _IdentityConverter extends convert.Converter { 74 | const _IdentityConverter(); 75 | @override 76 | Uint8List convert(Uint8List input) => input; 77 | } 78 | 79 | class _IdentityCodec extends convert.Codec { 80 | const _IdentityCodec(); 81 | @override 82 | convert.Converter get encoder => 83 | const _IdentityConverter(); 84 | @override 85 | convert.Converter get decoder => 86 | const _IdentityConverter(); 87 | } 88 | 89 | /// A key-value database 90 | class LevelDB extends NativeFieldWrapperClass2 { 91 | final convert.Codec _keyEncoding; 92 | final convert.Codec _valueEncoding; 93 | 94 | LevelDB._internal(this._keyEncoding, this._valueEncoding); 95 | 96 | void _open(bool shared, SendPort port, String path, int blockSize, 97 | bool createIfMissing, bool errorIfExists) native "DB_Open"; 98 | 99 | Uint8List? _syncGet(Uint8List key) native "SyncGet"; 100 | void _syncPut(Uint8List key, Uint8List value, bool sync) native "SyncPut"; 101 | void _syncDelete(Uint8List key) native "SyncDelete"; 102 | void _syncClose() native "SyncClose"; 103 | 104 | static LevelError? _getError(dynamic reply) { 105 | if (reply == -1) { 106 | return const LevelClosedError._internal(); 107 | } 108 | if (reply == -2) { 109 | return const LevelIOError._internal(); 110 | } 111 | if (reply == -3) { 112 | return const LevelCorruptionError._internal(); 113 | } 114 | if (reply == -4) { 115 | return const LevelInvalidArgumentError._internal(); 116 | } 117 | return null; 118 | } 119 | 120 | static bool _completeError(Completer completer, dynamic reply) { 121 | LevelError? e = _getError(reply); 122 | if (e != null) { 123 | completer.completeError(e); 124 | return true; 125 | } 126 | return false; 127 | } 128 | 129 | /// Default encoding. Expects to be passed a String and will encode/decode to UTF8 in the db. 130 | static convert.Codec get utf8 => 131 | const convert.Utf8Codec().fuse(const Uint8ListCodec()); 132 | 133 | /// Ascii encoding. Potentially faster than UTF8 for ascii-only text (untested). 134 | static convert.Codec get ascii => 135 | const convert.AsciiCodec().fuse(const Uint8ListCodec()); 136 | 137 | /// The identity encoding does no encoding. You must pass in a Uint8List to all functions. 138 | /// Because it does no transformation it reduces the number of allocations. 139 | /// Use this encoding for performance. 140 | static convert.Codec get identity => 141 | const _IdentityCodec(); 142 | 143 | /// Open a database at [path] using [String] keys and values which will be encoded to utf8 144 | /// in the database. 145 | /// 146 | /// See [open] for information on optional parameters. 147 | static Future> openUtf8(String path, 148 | {bool shared: false, 149 | int blockSize: 4096, 150 | bool createIfMissing: true, 151 | bool errorIfExists: false}) => 152 | open( 153 | path, 154 | shared: shared, 155 | blockSize: blockSize, 156 | createIfMissing: createIfMissing, 157 | errorIfExists: errorIfExists, 158 | keyEncoding: utf8, 159 | valueEncoding: utf8, 160 | ); 161 | 162 | /// Open a database at [path] using raw [Uint8List] keys and values. 163 | /// 164 | /// See [open] for information on optional parameters. 165 | static Future> openUint8List(String path, 166 | {bool shared: false, 167 | int blockSize: 4096, 168 | bool createIfMissing: true, 169 | bool errorIfExists: false}) => 170 | open(path, 171 | keyEncoding: identity, 172 | valueEncoding: identity, 173 | shared: shared, 174 | blockSize: blockSize, 175 | createIfMissing: createIfMissing, 176 | errorIfExists: errorIfExists); 177 | 178 | /// Open a database at [path] 179 | /// 180 | /// If [shared] is true the database will be shared to other isolates in the dart vm. The [LevelDB] returned 181 | /// in another isolate calling [open] with the same [path] will share the underlying database and data changes 182 | /// will be visible to both. 183 | /// 184 | /// [keyEncoding] or [valueEncoding] must be specified. The given encoding will 185 | /// be used to encoding and decode keys or values respectively. The encodings must match the generic 186 | /// type of the database. 187 | static Future> open(String path, 188 | {bool shared: false, 189 | int blockSize: 4096, 190 | bool createIfMissing: true, 191 | bool errorIfExists: false, 192 | required convert.Codec keyEncoding, 193 | required convert.Codec valueEncoding}) { 194 | Completer> completer = new Completer>(); 195 | RawReceivePort replyPort = new RawReceivePort(); 196 | LevelDB db = new LevelDB._internal(keyEncoding, valueEncoding); 197 | replyPort.handler = (dynamic result) { 198 | replyPort.close(); 199 | if (_completeError(completer, result)) { 200 | return; 201 | } 202 | completer.complete(db); 203 | }; 204 | db._open(shared, replyPort.sendPort, path, blockSize, createIfMissing, 205 | errorIfExists); 206 | return completer.future; 207 | } 208 | 209 | /// Close this database. 210 | /// Any pending iteration will throw after this call. 211 | void close() { 212 | _syncClose(); 213 | } 214 | 215 | /// Get a key in the database. Returns null if the key is not found. 216 | V? get(K key) { 217 | Uint8List keyEnc = _keyEncoding.encode(key); 218 | Uint8List? value = _syncGet(keyEnc); 219 | V? ret; 220 | if (value != null) { 221 | ret = _valueEncoding.decode(value); 222 | } 223 | return ret; 224 | } 225 | 226 | /// Set a key to a value. 227 | void put(K key, V value, {bool sync: false}) { 228 | Uint8List keyEnc = _keyEncoding.encode(key); 229 | Uint8List valueEnc = _valueEncoding.encode(value); 230 | _syncPut(keyEnc, valueEnc, sync); 231 | } 232 | 233 | /// Remove a key from the database 234 | void delete(K key) { 235 | Uint8List keyEnc = _keyEncoding.encode(key); 236 | _syncDelete(keyEnc); 237 | } 238 | 239 | /// Return an [Iterable] which will iterate through the db in key byte-collated order. 240 | /// 241 | /// To start iteration from a particular point use [gt] or [gte] and the iterator will start at the first key 242 | /// `>` or `>=` the passed value respectively. To stop iteration before the end use [lt] or [lte] to end at the 243 | /// key `<` or `<=` the passed value respectively. 244 | /// 245 | /// The [limit] parameter limits the total number of items iterated. 246 | /// 247 | /// For example, say a database contains the keys `a`, `b`, `c` and `d`. To iterate over all items from key `b` 248 | /// and before `d` in the collation order you can write: 249 | /// 250 | /// getItems(gte: 'b', lt: 'd') 251 | /// 252 | LevelIterable getItems( 253 | {K? gt, K? gte, K? lt, K? lte, int limit: -1, bool fillCache: true}) { 254 | return new LevelIterable._internal(this, limit, fillCache, 255 | gt == null ? gte : gt, gt == null, lt == null ? lte : lt, lt == null); 256 | } 257 | } 258 | 259 | /// A key-value pair returned by the iterator 260 | class LevelItem { 261 | /// The key. Type is determined by the keyEncoding specified 262 | final K key; 263 | 264 | /// The value. Type is determined by the valueEncoding specified 265 | final V value; 266 | LevelItem._internal(this.key, this.value); 267 | } 268 | 269 | /// An iterator 270 | class LevelIterator extends NativeFieldWrapperClass2 271 | implements Iterator> { 272 | final convert.Codec _keyEncoding; 273 | final convert.Codec _valueEncoding; 274 | 275 | LevelIterator._internal(LevelIterable it) 276 | : _keyEncoding = it._db._keyEncoding, 277 | _valueEncoding = it._db._valueEncoding; 278 | 279 | void _init(LevelDB db, int limit, bool fillCache, Uint8List? gt, 280 | bool isGtClosed, Uint8List? lt, bool isLtClosed) native "SyncIterator_New"; 281 | Uint8List? _next() native "SyncIterator_Next"; 282 | Uint8List? _current; 283 | 284 | /// The key of the current LevelItem 285 | K get currentKey { 286 | if (_current == null) { 287 | throw LevelInvalidIterator._internal(); 288 | } 289 | Uint8List current = _current!; 290 | return _keyEncoding.decode(new Uint8List.view( 291 | current.buffer, 4, (current[1] << 8) + current[0])); 292 | } 293 | 294 | /// The value of the current LevelItem 295 | V get currentValue { 296 | if (_current == null) { 297 | throw LevelInvalidIterator._internal(); 298 | } 299 | Uint8List current = _current!; 300 | return _valueEncoding.decode(new Uint8List.view( 301 | current.buffer, 4 + (current[3] << 8) + current[2])); 302 | } 303 | 304 | @override 305 | LevelItem get current { 306 | if (_current == null) { 307 | throw LevelInvalidIterator._internal(); 308 | } 309 | return LevelItem._internal(currentKey!, currentValue!); 310 | } 311 | 312 | @override 313 | bool moveNext() { 314 | _current = _next(); 315 | return _current != null; 316 | } 317 | } 318 | 319 | /// An [Iterable] for iterating over key-value pairs. 320 | /// 321 | /// Iteration is sorted by key in byte collation order. 322 | /// 323 | /// You can use the [keys] and [values] getters to get an [Iterable] over just the keys or just the values 324 | /// in the database. 325 | class LevelIterable extends IterableBase> { 326 | final LevelDB _db; 327 | 328 | final int _limit; 329 | final bool _fillCache; 330 | 331 | final K? _gt; 332 | final bool _isGtClosed; 333 | 334 | final K? _lt; 335 | final bool _isLtClosed; 336 | 337 | LevelIterable._internal(LevelDB db, int limit, bool fillCache, K? gt, 338 | bool isGtClosed, K? lt, bool isLtClosed) 339 | : _db = db, 340 | _limit = limit, 341 | _fillCache = fillCache, 342 | _gt = gt, 343 | _isGtClosed = isGtClosed, 344 | _lt = lt, 345 | _isLtClosed = isLtClosed; 346 | 347 | @override 348 | LevelIterator get iterator { 349 | LevelIterator ret = new LevelIterator._internal(this); 350 | Uint8List? ltEncoded; 351 | if (_lt != null) { 352 | ltEncoded = _db._keyEncoding.encode(_lt!); 353 | } 354 | Uint8List? gtEncoded; 355 | if (_gt != null) { 356 | gtEncoded = _db._keyEncoding.encode(_gt!); 357 | } 358 | 359 | ret._init(_db, _limit, _fillCache, gtEncoded, _isGtClosed, ltEncoded, 360 | _isLtClosed); 361 | return ret; 362 | } 363 | 364 | /// Returns an [Iterable] of the keys in the db 365 | Iterable get keys sync* { 366 | LevelIterator it = iterator; 367 | while (it.moveNext()) { 368 | yield it.currentKey; 369 | } 370 | } 371 | 372 | /// Returns an [Iterable] of the values in the db 373 | Iterable get values sync* { 374 | LevelIterator it = iterator; 375 | while (it.moveNext()) { 376 | yield it.currentValue; 377 | } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /lib/libleveldb.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamlofts/leveldb_dart/0329e5bbd3bde6d3aee766a785af3307936d9faa/lib/libleveldb.so -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: leveldb 2 | description: Dart bindings for the LevelDB key value store. LevelDB is a fast key/value data store which 3 | supports arbitrary byte arrays as both keys and values. 4 | version: 7.0.0 5 | homepage: https://github.com/adamlofts/leveldb_dart 6 | environment: 7 | sdk: '>=2.12.0 <2.15.0' 8 | 9 | dependencies: 10 | meta: ^1.1.6 11 | dev_dependencies: 12 | test: '>=1.3.0 <=1.17.0' 13 | -------------------------------------------------------------------------------- /test/leveldb_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:async'; 3 | import 'dart:convert'; 4 | import 'dart:isolate'; 5 | import 'dart:typed_data'; 6 | 7 | import 'package:test/test.dart'; 8 | import 'package:leveldb/leveldb.dart'; 9 | 10 | Future> _openTestDB( 11 | {int index: 0, bool shared: false, bool clean: true}) async { 12 | Directory d = new Directory('/tmp/test-level-db-dart-$index'); 13 | if (clean && d.existsSync()) { 14 | await d.delete(recursive: true); 15 | } 16 | return LevelDB.openUtf8('/tmp/test-level-db-dart-$index', shared: shared); 17 | } 18 | 19 | Future> _openTestDBEnc( 20 | Codec keyEncoding, Codec valueEncoding, 21 | {int index: 0, bool shared: false, bool clean: true}) async { 22 | Directory d = new Directory('/tmp/test-level-db-dart-$index'); 23 | if (clean && d.existsSync()) { 24 | await d.delete(recursive: true); 25 | } 26 | return LevelDB.open('/tmp/test-level-db-dart-$index', 27 | shared: shared, keyEncoding: keyEncoding, valueEncoding: valueEncoding); 28 | } 29 | 30 | const Matcher _isClosedError = const _ClosedMatcher(); 31 | 32 | class _ClosedMatcher extends TypeMatcher { 33 | const _ClosedMatcher(); 34 | } 35 | 36 | const Matcher _isIteratorError = const _IteratorMatcher(); 37 | 38 | class _IteratorMatcher extends TypeMatcher { 39 | const _IteratorMatcher(); 40 | } 41 | 42 | const Matcher _isInvalidArgumentError = const _InvalidArgumentMatcher(); 43 | 44 | class _InvalidArgumentMatcher extends TypeMatcher { 45 | const _InvalidArgumentMatcher(); 46 | } 47 | 48 | /// tests 49 | void main() { 50 | test('LevelDB basics', () async { 51 | LevelDB db = await _openTestDB(); 52 | 53 | db.put("k1", "v"); 54 | db.put("k2", "v"); 55 | 56 | expect(db.get("k1"), equals("v")); 57 | List keys = db.getItems().keys.toList(); 58 | expect(keys.first, equals("k1")); 59 | 60 | String? v = db.get("DOESNOTEXIST"); 61 | expect(v, equals(null)); 62 | 63 | // All keys 64 | keys = db.getItems().keys.toList(); 65 | expect(keys.length, equals(2)); 66 | keys = db.getItems(gte: "k1").keys.toList(); 67 | expect(keys.length, equals(2)); 68 | keys = db.getItems(gt: "k1").keys.toList(); 69 | expect(keys.length, equals(1)); 70 | 71 | keys = db.getItems(gt: "k0").keys.toList(); 72 | expect(keys.length, equals(2)); 73 | 74 | keys = db.getItems(gt: "k5").keys.toList(); 75 | expect(keys.length, equals(0)); 76 | keys = db.getItems(gte: "k5").keys.toList(); 77 | expect(keys.length, equals(0)); 78 | 79 | keys = db.getItems(limit: 1).keys.toList(); 80 | expect(keys.length, equals(1)); 81 | 82 | keys = db.getItems(lte: "k2").keys.toList(); 83 | expect(keys.length, equals(2)); 84 | keys = db.getItems(lt: "k2").keys.toList(); 85 | expect(keys.length, equals(1)); 86 | 87 | keys = db.getItems(gt: "k1", lt: "k2").keys.toList(); 88 | expect(keys.length, equals(0)); 89 | 90 | keys = db.getItems(gte: "k1", lt: "k2").keys.toList(); 91 | expect(keys.length, equals(1)); 92 | 93 | keys = db.getItems(gt: "k1", lte: "k2").keys.toList(); 94 | expect(keys.length, equals(1)); 95 | 96 | keys = db.getItems(gte: "k1", lte: "k2").keys.toList(); 97 | expect(keys.length, equals(2)); 98 | 99 | db.close(); 100 | 101 | LevelDB db2 = 102 | await _openTestDBEnc(LevelDB.identity, LevelDB.identity, clean: false); 103 | 104 | // Test with LevelEncodingNone 105 | Uint8List key = new Uint8List(2); 106 | key[0] = "k".codeUnitAt(0); 107 | key[1] = "1".codeUnitAt(0); 108 | keys = db2.getItems(gt: key).keys.toList(); 109 | expect(keys.length, equals(1)); 110 | 111 | keys = db2.getItems(gte: key).keys.toList(); 112 | expect(keys.length, equals(2)); 113 | 114 | key[1] = "2".codeUnitAt(0); 115 | keys = db2.getItems(gt: key).keys.toList(); 116 | expect(keys.length, equals(0)); 117 | 118 | keys = db2.getItems(gte: key).keys.toList(); 119 | expect(keys.length, equals(1)); 120 | 121 | keys = db2.getItems(lt: key).keys.toList(); 122 | expect(keys.length, equals(1)); 123 | 124 | keys = db2.getItems(lt: key).values.toList(); 125 | expect(keys.length, equals(1)); 126 | 127 | db2.close(); 128 | }); 129 | 130 | test('LevelDB delete', () async { 131 | LevelDB db = await _openTestDB(); 132 | try { 133 | db.put("k1", "v"); 134 | db.put("k2", "v"); 135 | 136 | db.delete("k1"); 137 | 138 | expect(db.get("k1"), equals(null)); 139 | expect(db.getItems().length, 1); 140 | } finally { 141 | db.close(); 142 | } 143 | }); 144 | 145 | test('TWO DBS', () async { 146 | LevelDB db1 = await _openTestDB(); 147 | LevelDB db2 = await _openTestDB(index: 1); 148 | 149 | db1.put("a", "1"); 150 | 151 | String? v = db2.get("a"); 152 | expect(v, equals(null)); 153 | 154 | db1.close(); 155 | db2.close(); 156 | }); 157 | 158 | test('Usage after close()', () async { 159 | LevelDB db1 = await _openTestDB(); 160 | db1.close(); 161 | 162 | expect(() => db1.get("SOME KEY"), throwsA(_isClosedError)); 163 | expect(() => db1.delete("SOME KEY"), throwsA(_isClosedError)); 164 | expect(() => db1.put("SOME KEY", "SOME KEY"), throwsA(_isClosedError)); 165 | expect(() => db1.close(), throwsA(_isClosedError)); 166 | 167 | try { 168 | for (LevelItem _ in db1.getItems()) { 169 | expect(true, equals(false)); // Should not happen. 170 | } 171 | } on LevelClosedError { 172 | expect(true, equals(true)); // Should happen. 173 | } 174 | }); 175 | 176 | test('DB locking throws IOError', () async { 177 | LevelDB db1 = await _openTestDB(); 178 | try { 179 | await _openTestDB(); 180 | expect(true, equals(false)); // Should not happen. The db is locked. 181 | } on LevelIOError { 182 | expect(true, equals(true)); // Should happen. 183 | } finally { 184 | db1.close(); 185 | } 186 | }); 187 | 188 | test('Exception inside iteration', () async { 189 | LevelDB db1 = await _openTestDB(); 190 | db1.put("a", "1"); 191 | db1.put("b", "1"); 192 | db1.put("c", "1"); 193 | 194 | try { 195 | for (LevelItem _ in db1.getItems()) { 196 | throw new Exception("OH NO"); 197 | } 198 | } catch (e) { 199 | // Pass 200 | } finally { 201 | db1.close(); 202 | } 203 | }); 204 | 205 | test('Test with None encoding', () async { 206 | LevelDB dbNone = 207 | await _openTestDBEnc(LevelDB.identity, LevelDB.identity, shared: true); 208 | LevelDB dbAscii = await _openTestDBEnc( 209 | LevelDB.ascii, LevelDB.ascii, 210 | shared: true, clean: false); 211 | LevelDB dbUtf8 = await _openTestDBEnc( 212 | LevelDB.utf8, LevelDB.utf8, 213 | shared: true, clean: false); 214 | Uint8List v = new Uint8List.fromList(utf8.encode("key1")); 215 | dbNone.put(v, v); 216 | 217 | String? s = dbUtf8.get("key1"); 218 | expect(s, equals("key1")); 219 | 220 | String? s2 = dbAscii.get("key1"); 221 | expect(s2, equals("key1")); 222 | 223 | Uint8List? v2 = dbNone.get(v); 224 | expect(v2, equals(v)); 225 | 226 | dbNone.delete(v); 227 | expect(dbNone.get(v), null); 228 | dbNone.close(); 229 | 230 | expect(dbAscii.get("key1"), null); 231 | dbAscii.close(); 232 | 233 | expect(dbUtf8.get("key1"), null); 234 | dbUtf8.close(); 235 | }); 236 | 237 | test('Close inside iteration', () async { 238 | LevelDB db1 = await _openTestDB(); 239 | db1.put("a", "1"); 240 | db1.put("b", "1"); 241 | 242 | bool isClosedSeen = false; 243 | 244 | try { 245 | for (LevelItem _ in db1.getItems()) { 246 | db1.close(); 247 | } 248 | } on LevelClosedError catch (_) { 249 | isClosedSeen = true; 250 | } 251 | 252 | expect(isClosedSeen, equals(true)); 253 | }); 254 | 255 | test('Test no create if missing', () async { 256 | expect( 257 | LevelDB.openUtf8('/tmp/test-level-db-dart-DOES-NOT-EXIST', 258 | createIfMissing: false), 259 | throwsA(_isInvalidArgumentError)); 260 | }); 261 | 262 | test('Test error if exists', () async { 263 | LevelDB db = 264 | await LevelDB.openUtf8('/tmp/test-level-db-dart-exists'); 265 | db.close(); 266 | expect( 267 | LevelDB.openUtf8('/tmp/test-level-db-dart-exists', errorIfExists: true), 268 | throwsA(_isInvalidArgumentError)); 269 | }); 270 | 271 | test('LevelDB sync iterator', () async { 272 | LevelDB db = await _openTestDB(); 273 | 274 | db.put("k1", "v"); 275 | db.put("k2", "v"); 276 | 277 | // All keys 278 | List> items1 = db.getItems().toList(); 279 | expect(items1.length, equals(2)); 280 | expect(items1.map((LevelItem i) => i.key).toList(), 281 | equals(["k1", "k2"])); 282 | expect(items1.map((LevelItem i) => i.value).toList(), 283 | equals(["v", "v"])); 284 | 285 | List> items = db.getItems(gte: "k1").toList(); 286 | expect(items.length, equals(2)); 287 | items = db.getItems(gt: "k1").toList(); 288 | expect(items.length, equals(1)); 289 | 290 | items = db.getItems(gt: "k0").toList(); 291 | expect(items.length, equals(2)); 292 | 293 | items = db.getItems(gt: "k5").toList(); 294 | expect(items.length, equals(0)); 295 | items = db.getItems(gte: "k5").toList(); 296 | expect(items.length, equals(0)); 297 | 298 | items = db.getItems(limit: 1).toList(); 299 | expect(items.length, equals(1)); 300 | 301 | items = db.getItems(lte: "k2").toList(); 302 | expect(items.length, equals(2)); 303 | items = db.getItems(lt: "k2").toList(); 304 | expect(items.length, equals(1)); 305 | 306 | items = db.getItems(gt: "k1", lt: "k2").toList(); 307 | expect(items.length, equals(0)); 308 | 309 | items = db.getItems(gte: "k1", lt: "k2").toList(); 310 | expect(items.length, equals(1)); 311 | 312 | items = db.getItems(gt: "k1", lte: "k2").toList(); 313 | expect(items.length, equals(1)); 314 | 315 | items = db.getItems(gte: "k1", lte: "k2").toList(); 316 | expect(items.length, equals(2)); 317 | 318 | String val = 319 | "bv-12345678901234567890123456789012345678901234567890123456789012345678901234567890"; 320 | db.put("a", val); 321 | LevelItem item = db.getItems(lte: "a").first; 322 | expect(item.value.length, val.length); 323 | 324 | String longKey = ""; 325 | for (int _ in new Iterable.generate(10)) { 326 | longKey += val; 327 | } 328 | db.put(longKey, longKey); 329 | item = db.getItems(gt: "a", lte: "c").first; 330 | expect(item.value.length, longKey.length); 331 | 332 | db.close(); 333 | }); 334 | 335 | test('LevelDB sync iterator use after close', () async { 336 | LevelDB db = await _openTestDB(); 337 | 338 | db.put("k1", "v"); 339 | db.put("k2", "v"); 340 | 341 | // All keys 342 | Iterator> it = db.getItems().iterator; 343 | it.moveNext(); 344 | 345 | db.close(); 346 | 347 | expect(() => it.moveNext(), throwsA(_isClosedError)); 348 | }); 349 | 350 | test('LevelDB sync iterator current == null', () async { 351 | LevelDB db = await _openTestDB(); 352 | 353 | db.put("k1", "v"); 354 | LevelIterator it = db.getItems().iterator; 355 | expect(() => it.current, throwsA(_isIteratorError)); 356 | expect(() => it.currentKey, throwsA(_isIteratorError)); 357 | expect(() => it.currentValue, throwsA(_isIteratorError)); 358 | 359 | it.moveNext(); 360 | expect(it.current.key, "k1"); 361 | expect(it.currentKey, "k1"); 362 | expect(it.currentValue, "v"); 363 | expect(it.moveNext(), false); 364 | expect(() => it.current, throwsA(_isIteratorError)); 365 | for (int _ in new Iterable.generate(10)) { 366 | expect(it.moveNext(), 367 | false); // Dart requires that it is safe to call moveNext after the end. 368 | expect(() => it.current, throwsA(_isIteratorError)); 369 | expect(() => it.currentKey, throwsA(_isIteratorError)); 370 | expect(() => it.currentValue, throwsA(_isIteratorError)); 371 | } 372 | db.close(); 373 | }); 374 | 375 | test('Shared db in same isolate', () async { 376 | LevelDB db = await _openTestDB(shared: true); 377 | LevelDB db1 = await _openTestDB(shared: true); 378 | 379 | db.put("k1", "v"); 380 | expect(db1.get("k1"), "v"); 381 | 382 | // Close the 1st reference. It cannot be used now. 383 | db.close(); 384 | expect(() => db.get("SOME KEY"), throwsA(_isClosedError)); 385 | 386 | // db1 Should still work. 387 | db1.put("k1", "v2"); 388 | expect(db1.get("k1"), "v2"); 389 | 390 | // close the 2nd reference. It cannot be used. 391 | db1.close(); 392 | expect(() => db1.get("SOME KEY"), throwsA(_isClosedError)); 393 | }); 394 | 395 | test('Shared db removed from map', () async { 396 | // Test that a shared db is correctly removed from the shared map when closed. 397 | LevelDB db = await _openTestDB(shared: true); 398 | db.close(); 399 | 400 | // Since the db is closed above it will be remove from the shared map and therefore 401 | // this will open a new db and we are allowed to read/write keys. 402 | LevelDB db1 = await _openTestDB(shared: true); 403 | db1.put("k1", "v"); 404 | expect(db1.get("k1"), "v"); 405 | }); 406 | 407 | test('Shared db isolates test', () async { 408 | // Spawn 2 isolates of which open and close the same shared db a lot in an attempt to find race conditions 409 | // in opening and closing the db. 410 | Future run(int index) { 411 | Completer completer = new Completer(); 412 | RawReceivePort exitPort = new RawReceivePort((dynamic _) { 413 | if (!completer.isCompleted) { 414 | completer.complete(); 415 | } 416 | }); 417 | RawReceivePort errorPort = 418 | new RawReceivePort((Object v) => completer.completeError(v)); 419 | Isolate.spawn(_isolateTest, index, 420 | onExit: exitPort.sendPort, onError: errorPort.sendPort); 421 | return completer.future; 422 | } 423 | 424 | await Future.wait(new Iterable.generate(2).map(run), eagerError: true); 425 | }); 426 | } 427 | 428 | // Must be a top-level because this function runs in another isolate. 429 | Future _isolateTest(int v) async { 430 | for (int _ in new Iterable.generate(1000)) { 431 | LevelDB db = await _openTestDB(shared: true, clean: false); 432 | // Allocate an iterator. 433 | for (LevelItem _ in db.getItems(limit: 2)) { 434 | // pass 435 | } 436 | db.close(); 437 | 438 | await new Future.delayed(new Duration(milliseconds: 2)); 439 | } 440 | } 441 | --------------------------------------------------------------------------------