├── .gitignore ├── .gitmodules ├── .npmignore ├── .travis.yml ├── History.md ├── Makefile ├── Readme.md ├── binding.gyp ├── deps └── sophia.cc.gyp ├── index.js ├── package.json ├── src ├── binding.cc ├── database.cc ├── database.h ├── database_workers.cc ├── database_workers.h ├── iterator.cc ├── iterator.h ├── iterator_workers.cc ├── iterator_workers.h ├── sophist.h ├── transaction.cc ├── transaction.h ├── transaction_workers.cc └── transaction_workers.h └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | testdb* 3 | node_modules/ 4 | coverage.html 5 | lib-cov/ 6 | npm-debug.log 7 | test-db 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/sophia"] 2 | path = deps/sophia 3 | url = https://github.com/pmwkaa/sophia.git 4 | [submodule "deps/sophia.cc"] 5 | path = deps/sophia.cc 6 | url = https://github.com/stephenmathieson/sophia.cc.git 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | testdb/ 3 | node_modules/ 4 | test/ 5 | coverage.html 6 | Makefile -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.11" 5 | script: REPORTER=dot make test 6 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 1.1.0 / 2014-12-12 3 | ================== 4 | 5 | * package: Update `mocha` to 2.0.1 6 | * History: Regenerate with `git changelog` 7 | * Readme: Fix whitespace 8 | * package: Update `nan` to 1.4.1 9 | * iterator: Add `lte` option support 10 | * package: Update `gnode` to 0.1.0 11 | 12 | 1.0.0 / 2014-09-18 13 | ================== 14 | 15 | * **breaking change:** Removed `levelup` support entirely 16 | * **breaking change:** Removed `db.purge()` and `db.clear()` 17 | * **breaking change:** Upgraded Sophia to [5a88904](https://github.com/pmwkaa/sophia/commit/5a88904e282d7f89422a005204ea7cc5f1785486) ([#4](https://github.com/stephenmathieson/node-sophist/issues/4)) 18 | * Add node 0.10.* and 0.11 support ([#5](https://github.com/stephenmathieson/node-sophist/issues/5) and [#11](https://github.com/stephenmathieson/node-sophist/issues/11)) 19 | * Add more synchronous methods ([#12](https://github.com/stephenmathieson/node-sophist/issues/12)): 20 | - `db.openSync()` 21 | - `db.closeSync()` 22 | - `iterator.nextSync()` 23 | - `iterator.endSync()` 24 | * Add `yield` support to asynchronous methods: 25 | - `yield db.open()` 26 | - `yield db.close()` 27 | - `yield db.get(key)` 28 | - `yield db.set(key, value)` 29 | - `yield iterator.next()` 30 | - `yield transaction.commit()` 31 | - `yield transaction.rollback()` 32 | * `db.close()` and `db.closeSync()` will now cleanup any iterators left open 33 | 34 | 0.2.2 / 2014-04-20 35 | ================== 36 | 37 | * Prevent segfaults when attempting to read/write an unopened db 38 | * Refactor to use [sophia.cc](https://github.com/stephenmathieson/sophia.cc) 39 | 40 | 0.2.1 / 2014-02-08 41 | ================== 42 | 43 | * Remove `package.json` from deps/list to work around [npm#4630](https://github.com/npm/npm/issues/4630). 44 | 45 | 0.2.0 / 2014-02-08 46 | ================== 47 | 48 | * Refactor transactions for nearly a 90% performance boost 49 | * Add `#deleteSync` 50 | * Change iteration end `null` -> `undefined` 51 | * Add primitive [LevelUP](https://github.com/rvagg/node-levelup) compatibility 52 | 53 | 0.1.0 / 2014-01-27 54 | ================== 55 | 56 | * Fix error handling (NAN expects weird stuff with `errmsg`) 57 | * Add options to `#open` 58 | * Add `#getSync` and `#setSync` 59 | * Add `#purge` 60 | * Add iteration support: 61 | - `#iterator` 62 | - `iterator#next` 63 | - `iterator#end` 64 | * Add transaction support: 65 | - `#transaction` 66 | - `transation#commit` 67 | - `transation#rollback` 68 | - `transation#set` 69 | - `transation#delete` 70 | 71 | 0.0.0 / 2014-01-23 72 | ================== 73 | 74 | * Initial release, supporting very basic (and buggy) operations -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | BIN := node_modules/.bin 3 | JS = $(wildcard index.js lib/*.js) 4 | SRC = $(wildcard src/*.*) 5 | TESTS = $(wildcard test/*.js) 6 | REPORTER ?= spec 7 | 8 | build: node_modules $(SRC) 9 | @node-gyp rebuild 10 | 11 | node_modules: package.json 12 | @npm install 13 | @touch $@ 14 | 15 | clean: 16 | @node-gyp clean 17 | @rm -rf testdb test-* 18 | 19 | test: build 20 | $(BIN)/gnode $(BIN)/_mocha \ 21 | --reporter $(REPORTER) \ 22 | --require co-mocha \ 23 | --slow 5ms 24 | 25 | valgrind: build 26 | @valgrind \ 27 | --leak-check=full \ 28 | --trace-children=yes \ 29 | node --harmony \ 30 | node_modules/.bin/_mocha \ 31 | --reporter spec \ 32 | --require co-mocha 33 | 34 | 35 | .PHONY: test clean valgrind 36 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # sophist 3 | 4 | An (unmaintained) Sophia binding. 5 | 6 | [![Build Status](https://travis-ci.org/stephenmathieson/node-sophist.png?branch=master)](https://travis-ci.org/stephenmathieson/node-sophist) 7 | 8 | ## API 9 | 10 | ### var db = new Sophist(path); 11 | 12 | Create a db instance at `path`. 13 | 14 | #### db.open([options], [fn]) / db.openSync([options]) 15 | 16 | Open the database, optionally with the given `options`. 17 | 18 | Options: 19 | 20 | * `createIfMissing`: boolean, default `true` 21 | * `readOnly`: boolean, default `false` 22 | * `pageSize`: number, default `2048` 23 | * `mergeWatermark`: number, default `100000` 24 | 25 | ```js 26 | yield db.open({ createIfMissing: false }); 27 | db.open(function (err) { /* ... */ }); 28 | db.openSync(); 29 | ``` 30 | 31 | #### db.close([fn]) / db.closeSync() 32 | 33 | Close the database. 34 | 35 | ```js 36 | yield db.close(); 37 | db.close(function (err) { /* ... */ }); 38 | db.closeSync(); 39 | ``` 40 | 41 | #### db.set(key, value, [fn]) / db.setSync(key, value) 42 | 43 | Set `key` to `value` in the database. 44 | 45 | ```js 46 | yield db.set('foo', 'bar'); 47 | db.set('foo', 'bar', function (err) { /* ... */ }); 48 | db.setSync('foo', 'bar'); 49 | ``` 50 | 51 | #### db.get(key, [fn]) / db.getSync(key) 52 | 53 | Get the value of `key`. 54 | 55 | ```js 56 | var value = yield db.get('foo'); 57 | db.get('foo', function (err, value) { /* ... */ }); 58 | var value = db.getSync('foo'); 59 | ``` 60 | 61 | #### db.delete(key, [fn]) / db.deleteSync(key) 62 | 63 | Delete `key` from the database. 64 | 65 | ```js 66 | var value = yield db.delete('foo'); 67 | db.delete('foo', function (err) { /* ... */ }); 68 | var value = db.deleteSync('foo'); 69 | ``` 70 | 71 | #### var iterator = db.iterator([options]) 72 | 73 | Create an iterator. 74 | 75 | **NOTE**: Sophia does *not* support writes while an iterator is open. 76 | 77 | Options: 78 | 79 | * `reverse`: boolean, default `false` 80 | * `start`: string, default `null` 81 | * `end`: string, default `null` 82 | * `gte`: boolean, default `false` 83 | * `lte`: boolean, default `false` 84 | 85 | ##### iterator.next([fn]) 86 | 87 | Get the next key/value pair from the iterator. 88 | 89 | Upon reaching the last key, `null`s will be provided. 90 | 91 | ```js 92 | var arr = yield iterator.next(); // [key, value] 93 | iterator.next(function (err, key, value) { /* ... */ }); 94 | ``` 95 | 96 | ##### iterator.end([fn]) 97 | 98 | End the iterator. 99 | 100 | ```js 101 | yield iterator.end(); 102 | iterator.end(function (err) { /* ... */ }); 103 | ``` 104 | 105 | #### var transaction = db.transaction() 106 | 107 | Create a Sophia transaction. 108 | 109 | During a transaction, all writes (`set` and `delete`) are posponed until `transaction#commit()` is called. Transaction writes may be reverted by calling `transaction#rollback()`. 110 | 111 | Unlike Sophia's raw C API, values set by `transaction#set()` and `transaction#get()` are **not** able to be retreived by `sophist#get()`. 112 | 113 | Sophia does not support nested or multiple transactions, so doing so will cause an error to be thrown. 114 | 115 | ```js 116 | var transaction = db.transaction(); 117 | var transaction2 = db.transaction(); // throws 118 | ``` 119 | 120 | ##### transaction.set(key, value) 121 | 122 | Push a `set` operation into the transaction. 123 | 124 | ```js 125 | yield db.set('foo', 'bar'); 126 | var transaction = db.transaction(); 127 | transaction.set('foo', 'baz'); 128 | var val = yield db.get('foo'); // bar 129 | yield transaction.commit(); 130 | var val = yield db.get('foo'); // baz 131 | ``` 132 | 133 | ##### transaction.delete(key) 134 | 135 | Push a `delete` operation into the transaction. 136 | 137 | ```js 138 | yield db.set('foo', 'bar'); 139 | var transaction = db.transaction(); 140 | transaction.delete('foo'); 141 | var val = yield db.get('foo'); // bar 142 | yield transaction.commit(); 143 | var val = yield db.get('foo'); // null 144 | ``` 145 | 146 | ##### transaction.commit([fn]) 147 | 148 | Commit the operations stored by the transaction. 149 | 150 | ```js 151 | var transaction = db.transaction(); 152 | // ... 153 | yield transaction.commit(); 154 | transaction.commit(function (err) { /* ... */ }); 155 | ``` 156 | 157 | ##### transaction.rollback([fn]) 158 | 159 | Rollback/revert the operations stored by the transaction. 160 | 161 | ```js 162 | var transaction = db.transaction(); 163 | // ... 164 | yield transaction.rollback(); 165 | transaction.rollback(function (err) { /* ... */ }); 166 | ``` 167 | 168 | ## License 169 | 170 | MIT 171 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': 'sophist', 5 | 'include_dirs': [ 6 | "", 6 | "keywords": [ 7 | "sophia", 8 | "sophiadb" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/stephenmathieson/node-sophist.git" 13 | }, 14 | "dependencies": { 15 | "bindings": "^1.2.1", 16 | "nan": "^1.4.1", 17 | "yieldly": "0.0.1" 18 | }, 19 | "devDependencies": { 20 | "co-mocha": "^1.0.0", 21 | "gnode": "^0.1.0", 22 | "matcha": "^0.5.0", 23 | "mocha": "^2.0.1", 24 | "rimraf": "^2.2.8" 25 | }, 26 | "main": "index", 27 | "license": "MIT", 28 | "scripts": { 29 | "install": "node-gyp rebuild" 30 | }, 31 | "gypfile": true 32 | } 33 | -------------------------------------------------------------------------------- /src/binding.cc: -------------------------------------------------------------------------------- 1 | 2 | #include "database.h" 3 | #include "iterator.h" 4 | #include "transaction.h" 5 | 6 | void InitSophist(v8::Handle exports) { 7 | sophist::Database::Init(exports); 8 | sophist::Iterator::Init(); 9 | sophist::Transaction::Init(); 10 | } 11 | 12 | NODE_MODULE(sophist, InitSophist); 13 | -------------------------------------------------------------------------------- /src/database.cc: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include "sophist.h" 4 | #include "database.h" 5 | #include "database_workers.h" 6 | #include "iterator.h" 7 | 8 | using namespace sophia; 9 | 10 | namespace sophist { 11 | 12 | static v8::Persistent database_constructor; 13 | 14 | Database::Database(char *path) : path(path) { 15 | currentIteratorId = 0; 16 | transaction = NULL; 17 | } 18 | 19 | Database::~Database() { 20 | delete[] path; 21 | delete sophia; 22 | } 23 | 24 | void Database::Init(v8::Handle exports) { 25 | v8::Local tpl = NanNew( 26 | Database::New 27 | ); 28 | NanAssignPersistent(database_constructor, tpl); 29 | 30 | tpl->SetClassName(NanNew("Database")); 31 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 32 | 33 | NODE_SET_PROTOTYPE_METHOD(tpl, "open", Database::Open); 34 | NODE_SET_PROTOTYPE_METHOD(tpl, "openSync", Database::OpenSync); 35 | NODE_SET_PROTOTYPE_METHOD(tpl, "close", Database::Close); 36 | NODE_SET_PROTOTYPE_METHOD(tpl, "closeSync", Database::CloseSync); 37 | NODE_SET_PROTOTYPE_METHOD(tpl, "set", Database::Set); 38 | NODE_SET_PROTOTYPE_METHOD(tpl, "setSync", Database::SetSync); 39 | NODE_SET_PROTOTYPE_METHOD(tpl, "getSync", Database::GetSync); 40 | NODE_SET_PROTOTYPE_METHOD(tpl, "get", Database::Get); 41 | NODE_SET_PROTOTYPE_METHOD(tpl, "deleteSync", Database::DeleteSync); 42 | NODE_SET_PROTOTYPE_METHOD(tpl, "delete", Database::Delete); 43 | NODE_SET_PROTOTYPE_METHOD(tpl, "iterator", Database::Iterator); 44 | NODE_SET_PROTOTYPE_METHOD(tpl, "transaction", Database::Transaction); 45 | 46 | exports->Set(NanNew("Database"), tpl->GetFunction()); 47 | } 48 | 49 | void Database::ReleaseIterator(uint32_t id) { 50 | iterators.erase(id); 51 | } 52 | 53 | void Database::ReleaseIterators() { 54 | // cleanup any open iterators 55 | if (!iterators.empty()) { 56 | std::map::iterator it; 57 | // loop, ending/releasing each open iterator 58 | for (it = iterators.begin(); it != iterators.end() ; ++it) { 59 | uint32_t id = it->first; 60 | sophist::Iterator *iterator = it->second; 61 | iterator->it->End(); 62 | ReleaseIterator(id); 63 | } 64 | } 65 | } 66 | 67 | NAN_METHOD(Database::New) { 68 | NanScope(); 69 | 70 | if (0 == args.Length() || !args[0]->IsString()) { 71 | NanThrowError("path required"); 72 | NanReturnUndefined(); 73 | } 74 | 75 | char *path; 76 | SP_V8_STRING_TO_CHAR_ARRAY(path, args[0]); 77 | Database *self = new Database(path); 78 | self->sophia = new Sophia(path); 79 | self->Wrap(args.This()); 80 | NanReturnValue(args.This()); 81 | } 82 | 83 | #define SP_PARSE_OPEN_OPTIONS() \ 84 | bool create_if_missing = NanBooleanOptionValue( \ 85 | options \ 86 | , NanNew("createIfMissing") \ 87 | , true \ 88 | ); \ 89 | bool read_only = NanBooleanOptionValue( \ 90 | options \ 91 | , NanNew("readOnly") \ 92 | , false \ 93 | ); \ 94 | uint32_t merge_watermark = NanUInt32OptionValue( \ 95 | options \ 96 | , NanNew("mergeWatermark") \ 97 | , 100000 \ 98 | ); \ 99 | uint32_t page_size = NanUInt32OptionValue( \ 100 | options \ 101 | , NanNew("pageSize") \ 102 | , 2048 \ 103 | ); 104 | 105 | NAN_METHOD(Database::OpenSync) { 106 | NanScope(); 107 | v8::Local options; 108 | if (args.Length() && args[0]->IsObject()) { 109 | options = args[0].As(); 110 | } 111 | SP_PARSE_OPEN_OPTIONS() 112 | Database *self = node::ObjectWrap::Unwrap(args.This()); 113 | SophiaReturnCode rc = self->sophia->Open( 114 | create_if_missing 115 | , read_only 116 | , page_size 117 | , merge_watermark 118 | ); 119 | if (SOPHIA_SUCCESS != rc) { 120 | NanThrowError(self->sophia->Error(rc)); 121 | } 122 | NanReturnUndefined(); 123 | } 124 | 125 | NAN_METHOD(Database::Open) { 126 | NanScope(); 127 | v8::Local options; 128 | v8::Local callback; 129 | 130 | if (0 == args.Length()) return NanThrowError("callback required"); 131 | if (args[0]->IsFunction()) { 132 | callback = args[0].As(); 133 | } else if (args[1]->IsFunction()) { 134 | options = args[0].As(); 135 | callback = args[1].As(); 136 | } else { 137 | return NanThrowError("callback required"); 138 | } 139 | 140 | SP_PARSE_OPEN_OPTIONS() 141 | Database *self = node::ObjectWrap::Unwrap(args.This()); 142 | OpenWorker *worker = new OpenWorker( 143 | self 144 | , new NanCallback(callback) 145 | , create_if_missing 146 | , read_only 147 | , merge_watermark 148 | , page_size 149 | ); 150 | // persist to prevent accidental GC 151 | v8::Local _this = args.This(); 152 | worker->SaveToPersistent("database", _this); 153 | NanAsyncQueueWorker(worker); 154 | NanReturnUndefined(); 155 | } 156 | 157 | #undef SP_PARSE_OPEN_OPTIONS 158 | 159 | NAN_METHOD(Database::CloseSync) { 160 | NanScope(); 161 | Database *self = node::ObjectWrap::Unwrap(args.This()); 162 | // cleanup iterators (if any) 163 | self->ReleaseIterators(); 164 | SophiaReturnCode rc = self->sophia->Close(); 165 | if (SOPHIA_SUCCESS != rc) { 166 | NanThrowError(self->sophia->Error(rc)); 167 | } 168 | NanReturnUndefined(); 169 | } 170 | 171 | NAN_METHOD(Database::Close) { 172 | NanScope(); 173 | 174 | if (0 == args.Length() || !args[0]->IsFunction()) { 175 | return NanThrowError("callback required"); 176 | } 177 | 178 | v8::Local callback = args[0].As(); 179 | Database *self = node::ObjectWrap::Unwrap(args.This()); 180 | 181 | CloseWorker *worker = new CloseWorker( 182 | self 183 | , new NanCallback(callback) 184 | ); 185 | // persist to prevent accidental GC 186 | v8::Local _this = args.This(); 187 | worker->SaveToPersistent("database", _this); 188 | NanAsyncQueueWorker(worker); 189 | NanReturnUndefined(); 190 | } 191 | 192 | NAN_METHOD(Database::SetSync) { 193 | NanScope(); 194 | Database *self = node::ObjectWrap::Unwrap(args.This()); 195 | 196 | if (2 > args.Length() || !args[0]->IsString() || !args[1]->IsString()) { 197 | return NanThrowError("key/value required"); 198 | } 199 | 200 | char *key, *value; 201 | SP_V8_STRING_TO_CHAR_ARRAY(key, args[0]); 202 | SP_V8_STRING_TO_CHAR_ARRAY(value, args[1]); 203 | 204 | SophiaReturnCode rc = self->sophia->Set(key, value); 205 | 206 | delete[] key; 207 | delete[] value; 208 | 209 | if (SOPHIA_SUCCESS != rc) { 210 | NanThrowError(self->sophia->Error(rc)); 211 | } 212 | 213 | NanReturnUndefined(); 214 | } 215 | 216 | NAN_METHOD(Database::Set) { 217 | NanScope(); 218 | 219 | if (3 > args.Length()) return NanThrowError("insufficient arguments"); 220 | if (!args[0]->IsString() || !args[1]->IsString()) { 221 | return NanThrowError("key/value required"); 222 | } 223 | if (!args[2]->IsFunction()) return NanThrowError("callback required"); 224 | 225 | Database *self = node::ObjectWrap::Unwrap(args.This()); 226 | char *key, *value; 227 | SP_V8_STRING_TO_CHAR_ARRAY(key, args[0]); 228 | SP_V8_STRING_TO_CHAR_ARRAY(value, args[1]); 229 | v8::Local callback = args[2].As(); 230 | 231 | SetWorker *worker = new SetWorker( 232 | self 233 | , new NanCallback(callback) 234 | , key 235 | , value 236 | ); 237 | // persist to prevent accidental GC 238 | v8::Local _this = args.This(); 239 | worker->SaveToPersistent("database", _this); 240 | NanAsyncQueueWorker(worker); 241 | NanReturnUndefined(); 242 | } 243 | 244 | NAN_METHOD(Database::GetSync) { 245 | NanScope(); 246 | 247 | Database *self = node::ObjectWrap::Unwrap(args.This()); 248 | 249 | if (0 == args.Length() || !args[0]->IsString()) { 250 | return NanThrowError("key required"); 251 | } 252 | 253 | char *key; 254 | SP_V8_STRING_TO_CHAR_ARRAY(key, args[0]); 255 | 256 | char *value = self->sophia->Get(key); 257 | 258 | delete[] key; 259 | 260 | if (value) { 261 | NanReturnValue(NanNew(value)); 262 | } else { 263 | NanReturnNull(); 264 | } 265 | } 266 | 267 | NAN_METHOD(Database::Get) { 268 | NanScope(); 269 | 270 | if (2 > args.Length() || !args[0]->IsString() || !args[1]->IsFunction()) { 271 | return NanThrowError("key/callback required"); 272 | } 273 | 274 | Database *self = node::ObjectWrap::Unwrap(args.This()); 275 | char *key; 276 | SP_V8_STRING_TO_CHAR_ARRAY(key, args[0]); 277 | v8::Local callback = args[1].As(); 278 | 279 | GetWorker *worker = new GetWorker( 280 | self 281 | , new NanCallback(callback) 282 | , key 283 | ); 284 | // persist to prevent accidental GC 285 | v8::Local _this = args.This(); 286 | worker->SaveToPersistent("database", _this); 287 | NanAsyncQueueWorker(worker); 288 | NanReturnUndefined(); 289 | } 290 | 291 | NAN_METHOD(Database::DeleteSync) { 292 | NanScope(); 293 | 294 | Database *self = node::ObjectWrap::Unwrap(args.This()); 295 | 296 | if (0 == args.Length() || !args[0]->IsString()) { 297 | return NanThrowError("key required"); 298 | } 299 | 300 | char *key; 301 | SP_V8_STRING_TO_CHAR_ARRAY(key, args[0]); 302 | 303 | SophiaReturnCode rc = self->sophia->Delete(key); 304 | 305 | delete[] key; 306 | 307 | if (SOPHIA_SUCCESS != rc) { 308 | NanThrowError(self->sophia->Error(rc)); 309 | } 310 | 311 | NanReturnUndefined(); 312 | } 313 | 314 | NAN_METHOD(Database::Delete) { 315 | NanScope(); 316 | 317 | if (2 > args.Length() || !args[0]->IsString() || !args[1]->IsFunction()) { 318 | return NanThrowError("key/callback required"); 319 | } 320 | 321 | Database *self = node::ObjectWrap::Unwrap(args.This()); 322 | char *key; 323 | SP_V8_STRING_TO_CHAR_ARRAY(key, args[0]); 324 | v8::Local callback = args[1].As(); 325 | 326 | DeleteWorker *worker = new DeleteWorker( 327 | self 328 | , new NanCallback(callback) 329 | , key 330 | ); 331 | // persist to prevent accidental GC 332 | v8::Local _this = args.This(); 333 | worker->SaveToPersistent("database", _this); 334 | NanAsyncQueueWorker(worker); 335 | NanReturnUndefined(); 336 | } 337 | 338 | NAN_METHOD(Database::Iterator) { 339 | NanScope(); 340 | v8::Local options; 341 | if (args.Length() && args[0]->IsObject()) { 342 | options = args[0].As(); 343 | } 344 | Database *self = node::ObjectWrap::Unwrap(args.This()); 345 | uint32_t id = self->currentIteratorId++; 346 | 347 | v8::TryCatch trycatch; 348 | v8::Local iteratorHandle = Iterator::NewInstance( 349 | args.This() 350 | , NanNew(id) 351 | , options 352 | ); 353 | 354 | // rethrow any caught exceptions to 355 | // avoid fatal errors in node::ObjectWrap 356 | if (trycatch.HasCaught()) { 357 | return NanThrowError(trycatch.Exception()); 358 | } 359 | 360 | sophist::Iterator *iterator = node::ObjectWrap::Unwrap( 361 | iteratorHandle 362 | ); 363 | self->iterators[id] = iterator; 364 | NanReturnValue(iteratorHandle); 365 | } 366 | 367 | NAN_METHOD(Database::Transaction) { 368 | NanScope(); 369 | Database *self = node::ObjectWrap::Unwrap(args.This()); 370 | if (NULL != self->transaction) { 371 | return NanThrowError("another transaction is already open"); 372 | } 373 | v8::Local transactionHandle = Transaction::NewInstance( 374 | args.This() 375 | ); 376 | sophist::Transaction *transaction = 377 | node::ObjectWrap::Unwrap( 378 | transactionHandle 379 | ); 380 | self->transaction = transaction; 381 | NanReturnValue(transactionHandle); 382 | } 383 | 384 | }; // namespace sophist 385 | -------------------------------------------------------------------------------- /src/database.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef DATABASE_H 3 | #define DATABASE_H 1 4 | 5 | #include 6 | #include 7 | #include 8 | #include "sophia-cc.h" 9 | #include "iterator.h" 10 | #include "transaction.h" 11 | 12 | namespace sophist { 13 | 14 | class Database : public node::ObjectWrap { 15 | public: 16 | Database(char *path); 17 | ~Database(); 18 | 19 | static void Init(v8::Handle exports); 20 | void ReleaseIterator(uint32_t id); 21 | void ReleaseIterators(); 22 | 23 | char *path; 24 | sophia::Sophia *sophia; 25 | std::map iterators; 26 | class Transaction *transaction; 27 | 28 | private: 29 | static NAN_METHOD(New); 30 | static NAN_METHOD(Open); 31 | static NAN_METHOD(OpenSync); 32 | static NAN_METHOD(Close); 33 | static NAN_METHOD(CloseSync); 34 | static NAN_METHOD(Set); 35 | static NAN_METHOD(SetSync); 36 | static NAN_METHOD(Get); 37 | static NAN_METHOD(GetSync); 38 | static NAN_METHOD(Delete); 39 | static NAN_METHOD(DeleteSync); 40 | static NAN_METHOD(Iterator); 41 | static NAN_METHOD(Transaction); 42 | 43 | uint32_t currentIteratorId; 44 | }; 45 | 46 | }; // namespace sophist 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /src/database_workers.cc: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include "sophia-cc.h" 4 | #include "database.h" 5 | #include "database_workers.h" 6 | 7 | using namespace sophia; 8 | 9 | namespace sophist { 10 | 11 | /** 12 | * Open worker. 13 | */ 14 | 15 | OpenWorker::OpenWorker( 16 | Database *database 17 | , NanCallback *callback 18 | , bool create_if_missing 19 | , bool read_only 20 | , uint32_t merge_watermark 21 | , uint32_t page_size 22 | ) : DatabaseWorker(database, callback) 23 | , create_if_missing(create_if_missing) 24 | , read_only(read_only) 25 | , merge_watermark(merge_watermark) 26 | , page_size(page_size) {} 27 | 28 | void OpenWorker::Execute() { 29 | SophiaReturnCode rc = database->sophia->Open( 30 | create_if_missing 31 | , read_only 32 | , page_size 33 | , merge_watermark 34 | ); 35 | if (SOPHIA_SUCCESS != rc) { 36 | SetErrorMessage(database->sophia->Error(rc)); 37 | } 38 | } 39 | 40 | /** 41 | * Close worker. 42 | */ 43 | 44 | CloseWorker::CloseWorker( 45 | Database *database 46 | , NanCallback *callback 47 | ) : DatabaseWorker(database, callback) {} 48 | 49 | void CloseWorker::Execute() { 50 | // cleanup any open iterators 51 | database->ReleaseIterators(); 52 | SophiaReturnCode rc = database->sophia->Close(); 53 | if (SOPHIA_SUCCESS != rc) { 54 | SetErrorMessage(database->sophia->Error(rc)); 55 | } 56 | } 57 | 58 | /** 59 | * Set worker. 60 | */ 61 | 62 | SetWorker::SetWorker( 63 | Database *database 64 | , NanCallback *callback 65 | , char *key 66 | , char *value 67 | ) : DatabaseWorker(database, callback), key(key), value(value) {} 68 | 69 | SetWorker::~SetWorker() { 70 | delete[] key; 71 | delete[] value; 72 | } 73 | 74 | void SetWorker::Execute() { 75 | SophiaReturnCode rc = database->sophia->Set(key, value); 76 | if (SOPHIA_SUCCESS != rc) { 77 | SetErrorMessage(database->sophia->Error(rc)); 78 | } 79 | } 80 | 81 | /** 82 | * Get worker. 83 | */ 84 | 85 | GetWorker::GetWorker( 86 | Database *database 87 | , NanCallback *callback 88 | , char *key 89 | ) : DatabaseWorker(database, callback), key(key) {} 90 | 91 | GetWorker::~GetWorker() { 92 | delete[] key; 93 | if (value) free(value); 94 | } 95 | 96 | void GetWorker::Execute() { 97 | value = database->sophia->Get(key); 98 | } 99 | 100 | void GetWorker::HandleOKCallback() { 101 | NanScope(); 102 | 103 | v8::Local ret; 104 | if (value) { 105 | ret = NanNew(value); 106 | } else { 107 | ret = NanNull(); 108 | } 109 | 110 | v8::Local argv[] = { 111 | NanNull() 112 | , ret 113 | }; 114 | callback->Call(2, argv); 115 | } 116 | 117 | /** 118 | * Delete worker. 119 | */ 120 | 121 | DeleteWorker::DeleteWorker( 122 | Database *database 123 | , NanCallback *callback 124 | , char *key 125 | ) : DatabaseWorker(database, callback), key(key) {} 126 | 127 | DeleteWorker::~DeleteWorker() { 128 | delete[] key; 129 | } 130 | 131 | void DeleteWorker::Execute() { 132 | SophiaReturnCode rc = database->sophia->Delete(key); 133 | if (SOPHIA_SUCCESS != rc) { 134 | SetErrorMessage(database->sophia->Error(rc)); 135 | } 136 | } 137 | 138 | } // namespace sophist 139 | -------------------------------------------------------------------------------- /src/database_workers.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef DATABASE_WORKERS_H 3 | #define DATABASE_WORKERS_H 1 4 | 5 | #include 6 | 7 | namespace sophist { 8 | 9 | /** 10 | * Abstract Database worker. 11 | */ 12 | 13 | class DatabaseWorker : public NanAsyncWorker { 14 | public: 15 | DatabaseWorker( 16 | Database *database 17 | , NanCallback *callback 18 | ) : NanAsyncWorker(callback), database(database) {} 19 | 20 | protected: 21 | Database *database; 22 | }; 23 | 24 | class OpenWorker : public DatabaseWorker { 25 | public: 26 | OpenWorker( 27 | Database *database 28 | , NanCallback *callback 29 | , bool create_if_missing 30 | , bool read_only 31 | , uint32_t merge_watermark 32 | , uint32_t page_size 33 | ); 34 | 35 | virtual void Execute(); 36 | 37 | private: 38 | bool create_if_missing; 39 | bool read_only; 40 | uint32_t merge_watermark; 41 | uint32_t page_size; 42 | }; 43 | 44 | class CloseWorker : public DatabaseWorker { 45 | public: 46 | CloseWorker( 47 | Database *database 48 | , NanCallback *callback 49 | ); 50 | 51 | virtual void Execute(); 52 | }; 53 | 54 | class SetWorker : public DatabaseWorker { 55 | public: 56 | SetWorker( 57 | Database *database 58 | , NanCallback *callback 59 | , char *key 60 | , char *value 61 | ); 62 | 63 | virtual ~SetWorker(); 64 | virtual void Execute(); 65 | 66 | private: 67 | char *key; 68 | char *value; 69 | }; 70 | 71 | class GetWorker : public DatabaseWorker { 72 | public: 73 | GetWorker( 74 | Database *database 75 | , NanCallback *callback 76 | , char *key 77 | ); 78 | 79 | virtual ~GetWorker(); 80 | virtual void HandleOKCallback(); 81 | virtual void Execute(); 82 | 83 | private: 84 | char *key; 85 | char *value; 86 | }; 87 | 88 | 89 | class DeleteWorker : public DatabaseWorker { 90 | public: 91 | DeleteWorker( 92 | Database *database 93 | , NanCallback *callback 94 | , char *key 95 | ); 96 | 97 | virtual ~DeleteWorker(); 98 | virtual void Execute(); 99 | 100 | private: 101 | char *key; 102 | }; 103 | 104 | }; // namespace sophist 105 | 106 | #endif 107 | -------------------------------------------------------------------------------- /src/iterator.cc: -------------------------------------------------------------------------------- 1 | 2 | #include "sophist.h" 3 | #include "iterator.h" 4 | #include "iterator_workers.h" 5 | 6 | namespace sophist { 7 | 8 | static v8::Persistent iterator_constructor; 9 | 10 | Iterator::Iterator( 11 | Database *database 12 | , uint32_t id 13 | , bool reverse 14 | , char *start 15 | , char *end 16 | , bool gte 17 | , bool lte 18 | ) : database(database) 19 | , id(id) 20 | , reverse(reverse) 21 | , start(start) 22 | , end(end) 23 | , gte(gte) 24 | , lte(lte) {} 25 | 26 | Iterator::~Iterator() { 27 | if (start) delete[] start; 28 | if (end) delete[] end; 29 | } 30 | 31 | void Iterator::Init() { 32 | v8::Local tpl = NanNew( 33 | Iterator::New 34 | ); 35 | NanAssignPersistent(iterator_constructor, tpl); 36 | tpl->SetClassName(NanNew("Iterator")); 37 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 38 | NODE_SET_PROTOTYPE_METHOD(tpl, "next", Iterator::Next); 39 | NODE_SET_PROTOTYPE_METHOD(tpl, "nextSync", Iterator::NextSync); 40 | NODE_SET_PROTOTYPE_METHOD(tpl, "end", Iterator::End); 41 | NODE_SET_PROTOTYPE_METHOD(tpl, "endSync", Iterator::EndSync); 42 | } 43 | 44 | v8::Local Iterator::NewInstance( 45 | v8::Local database 46 | , v8::Local id 47 | , v8::Local options 48 | ) { 49 | NanEscapableScope(); 50 | 51 | v8::Local instance; 52 | v8::Local constructor = NanNew( 53 | iterator_constructor 54 | ); 55 | 56 | if (options.IsEmpty()) { 57 | v8::Handle argv[2] = { database, id }; 58 | instance = constructor->GetFunction()->NewInstance(2, argv); 59 | } else { 60 | v8::Handle argv[3] = { database, id, options }; 61 | instance = constructor->GetFunction()->NewInstance(3, argv); 62 | } 63 | 64 | return NanEscapeScope(instance); 65 | } 66 | 67 | NAN_METHOD(Iterator::New) { 68 | NanScope(); 69 | Database *database = node::ObjectWrap::Unwrap( 70 | args[0]->ToObject() 71 | ); 72 | v8::Local id = args[1]; 73 | v8::Local options; 74 | bool reverse = false; 75 | char *start = NULL; 76 | char *end = NULL; 77 | bool gte = false; 78 | bool lte = false; 79 | 80 | if (args.Length() > 1 && args[2]->IsObject()) { 81 | options = v8::Local::Cast(args[2]); 82 | reverse = NanBooleanOptionValue(options, NanNew("reverse")); 83 | 84 | #define STRING_OPTION(name) \ 85 | v8::Local _ ## name = NanNew(#name); \ 86 | if (options->Has(_ ## name)) { \ 87 | if (!options->Get(_ ## name)->IsString()) { \ 88 | return NanThrowError(#name " key must be a string"); \ 89 | } \ 90 | SP_V8_STRING_TO_CHAR_ARRAY(name, options->Get(_ ## name)); \ 91 | } \ 92 | 93 | STRING_OPTION(start) 94 | STRING_OPTION(end) 95 | 96 | #undef STRING_OPTION 97 | 98 | gte = NanBooleanOptionValue(options, NanNew("gte")); 99 | lte = NanBooleanOptionValue(options, NanNew("lte")); 100 | } 101 | 102 | Iterator *iterator = new Iterator( 103 | database 104 | , (uint32_t) id->Int32Value() 105 | , reverse 106 | , start 107 | , end 108 | , gte 109 | , lte 110 | ); 111 | 112 | sporder order = reverse 113 | ? lte ? SPLTE : SPLT 114 | : gte ? SPGTE : SPGT; 115 | 116 | iterator->it = new sophia::Iterator( 117 | database->sophia 118 | , order 119 | , start 120 | , end 121 | ); 122 | 123 | sophia::SophiaReturnCode rc = iterator->it->Begin(); 124 | if (sophia::SOPHIA_SUCCESS != rc) { 125 | NanThrowError(database->sophia->Error(rc)); 126 | } 127 | 128 | iterator->Wrap(args.This()); 129 | NanReturnValue(args.This()); 130 | } 131 | 132 | NAN_METHOD(Iterator::Next) { 133 | NanScope(); 134 | 135 | if (0 == args.Length() || !args[0]->IsFunction()) { 136 | NanThrowError("callback required"); 137 | NanReturnUndefined(); 138 | } 139 | 140 | Iterator *iterator = node::ObjectWrap::Unwrap(args.This()); 141 | v8::Local callback = args[0].As(); 142 | NextWorker *worker = new NextWorker( 143 | iterator 144 | , new NanCallback(callback) 145 | ); 146 | // persist to prevent accidental GC 147 | v8::Local _this = args.This(); 148 | worker->SaveToPersistent("iterator", _this); 149 | NanAsyncQueueWorker(worker); 150 | 151 | NanReturnValue(args.Holder()); 152 | } 153 | 154 | NAN_METHOD(Iterator::NextSync) { 155 | NanScope(); 156 | Iterator *iterator = node::ObjectWrap::Unwrap(args.This()); 157 | sophia::IteratorResult *result = iterator->it->Next(); 158 | 159 | v8::Local arr = NanNew(2); 160 | arr->Set(0, NanNew(result->key)); 161 | arr->Set(1, NanNew(result->value)); 162 | delete result; 163 | NanReturnValue(arr); 164 | } 165 | 166 | NAN_METHOD(Iterator::End) { 167 | NanScope(); 168 | 169 | if (0 == args.Length() || !args[0]->IsFunction()) { 170 | NanThrowError("callback required"); 171 | NanReturnUndefined(); 172 | } 173 | 174 | Iterator *iterator = node::ObjectWrap::Unwrap(args.This()); 175 | v8::Local callback = args[0].As(); 176 | EndWorker *worker = new EndWorker( 177 | iterator 178 | , new NanCallback(callback) 179 | ); 180 | // persist to prevent accidental GC 181 | v8::Local _this = args.This(); 182 | worker->SaveToPersistent("iterator", _this); 183 | NanAsyncQueueWorker(worker); 184 | 185 | NanReturnValue(args.Holder()); 186 | } 187 | 188 | NAN_METHOD(Iterator::EndSync) { 189 | NanScope(); 190 | Iterator *iterator = node::ObjectWrap::Unwrap(args.This()); 191 | sophia::SophiaReturnCode rc = iterator->it->End(); 192 | if (sophia::SOPHIA_SUCCESS != rc) { 193 | return NanThrowError(iterator->database->sophia->Error(rc)); 194 | } 195 | iterator->database->ReleaseIterator(iterator->id); 196 | NanReturnUndefined(); 197 | } 198 | 199 | } // namespace sophist 200 | -------------------------------------------------------------------------------- /src/iterator.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef ITERATOR_H 3 | #define ITERATOR_H 1 4 | 5 | #include "database.h" 6 | 7 | namespace sophist { 8 | 9 | class Database; 10 | 11 | class Iterator : public node::ObjectWrap { 12 | public: 13 | Iterator( 14 | Database *database 15 | , uint32_t id 16 | , bool reverse 17 | , char *start 18 | , char *end 19 | , bool gte 20 | , bool lte 21 | ); 22 | ~Iterator(); 23 | static void Init(); 24 | static v8::Local NewInstance( 25 | v8::Local database 26 | , v8::Local id 27 | , v8::Local options 28 | ); 29 | 30 | Database *database; 31 | sophia::Iterator *it; 32 | uint32_t id; 33 | 34 | private: 35 | static NAN_METHOD(New); 36 | static NAN_METHOD(Next); 37 | static NAN_METHOD(NextSync); 38 | static NAN_METHOD(End); 39 | static NAN_METHOD(EndSync); 40 | 41 | protected: 42 | bool reverse; 43 | char *start; 44 | char *end; 45 | bool gte; 46 | bool lte; 47 | }; 48 | 49 | } // namespace sophist 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /src/iterator_workers.cc: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include "sophia-cc.h" 4 | #include "iterator_workers.h" 5 | 6 | #include 7 | 8 | using namespace sophia; 9 | 10 | namespace sophist { 11 | 12 | NextWorker::NextWorker( 13 | Iterator *iterator 14 | , NanCallback *callback 15 | ) : IteratorWorker(iterator, callback) {} 16 | 17 | void NextWorker::Execute() { 18 | result = iterator->it->Next(); 19 | } 20 | 21 | void NextWorker::HandleOKCallback() { 22 | if (result) { 23 | v8::Local argv[] = { 24 | NanNull() 25 | , NanNew(result->key) 26 | , NanNew(result->value) 27 | }; 28 | delete result; 29 | callback->Call(3, argv); 30 | } else { 31 | v8::Local argv[] = { NanNull() }; 32 | callback->Call(1, argv); 33 | } 34 | } 35 | 36 | EndWorker::EndWorker( 37 | Iterator *iterator 38 | , NanCallback *callback 39 | ) : IteratorWorker(iterator, callback) {} 40 | 41 | void EndWorker::Execute() { 42 | sophia::SophiaReturnCode rc = iterator->it->End(); 43 | if (SOPHIA_SUCCESS != rc) { 44 | SetErrorMessage(iterator->database->sophia->Error(rc)); 45 | } 46 | } 47 | 48 | void EndWorker::HandleOKCallback() { 49 | // destroy the iterator on the db side 50 | uint32_t id = iterator->id; 51 | iterator->database->ReleaseIterator(id); 52 | // normal callback 53 | v8::Local argv[] = { NanNull() }; 54 | callback->Call(1, argv); 55 | } 56 | 57 | } // namespace sophist 58 | -------------------------------------------------------------------------------- /src/iterator_workers.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef ITERATOR_WORKERS_H 3 | #define ITERATOR_WORKERS_H 1 4 | 5 | #include 6 | #include "sophia-cc.h" 7 | #include "iterator.h" 8 | 9 | namespace sophist { 10 | 11 | /** 12 | * Abstract Iterator worker. 13 | */ 14 | 15 | class IteratorWorker : public NanAsyncWorker { 16 | public: 17 | IteratorWorker( 18 | Iterator *iterator 19 | , NanCallback *callback 20 | ) : NanAsyncWorker(callback), iterator(iterator) {} 21 | 22 | protected: 23 | Iterator *iterator; 24 | }; 25 | 26 | class NextWorker : public IteratorWorker { 27 | public: 28 | NextWorker( 29 | Iterator *iterator 30 | , NanCallback *callback 31 | ); 32 | 33 | virtual void Execute(); 34 | virtual void HandleOKCallback(); 35 | 36 | private: 37 | sophia::IteratorResult *result; 38 | }; 39 | 40 | class EndWorker : public IteratorWorker { 41 | public: 42 | EndWorker( 43 | Iterator *iterator 44 | , NanCallback *callback 45 | ); 46 | 47 | virtual void Execute(); 48 | virtual void HandleOKCallback(); 49 | }; 50 | 51 | } // namespace sophist 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /src/sophist.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef SOPHIST_H 3 | #define SOPHIST_H 1 4 | 5 | namespace sophist { 6 | 7 | #define SP_V8_STRING_TO_CHAR_ARRAY(name, from) \ 8 | size_t name ## _size; \ 9 | v8::Local name ## _str = from->ToString(); \ 10 | name ## _size = name ## _str->Utf8Length() + 1; \ 11 | name = new char[name ## _size]; \ 12 | name ## _str->WriteUtf8( \ 13 | name \ 14 | , name ## _size \ 15 | ); 16 | 17 | } // namespace sophist 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /src/transaction.cc: -------------------------------------------------------------------------------- 1 | 2 | #include "transaction.h" 3 | #include "transaction_workers.h" 4 | #include "sophist.h" 5 | 6 | namespace sophist { 7 | 8 | static v8::Persistent transaction_constructor; 9 | 10 | Transaction::Transaction(Database *database) : database(database) {} 11 | 12 | Transaction::~Transaction() { 13 | delete t; 14 | } 15 | 16 | void Transaction::Init() { 17 | v8::Local tpl = NanNew( 18 | Transaction::New 19 | ); 20 | NanAssignPersistent(transaction_constructor, tpl); 21 | tpl->SetClassName(NanNew("Transaction")); 22 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 23 | NODE_SET_PROTOTYPE_METHOD(tpl, "set", Transaction::Set); 24 | NODE_SET_PROTOTYPE_METHOD(tpl, "delete", Transaction::Delete); 25 | NODE_SET_PROTOTYPE_METHOD(tpl, "commit", Transaction::Commit); 26 | NODE_SET_PROTOTYPE_METHOD(tpl, "rollback", Transaction::Rollback); 27 | } 28 | 29 | v8::Local Transaction::NewInstance( 30 | v8::Local database 31 | ) { 32 | NanEscapableScope(); 33 | 34 | v8::Local instance; 35 | v8::Local c = NanNew( 36 | transaction_constructor 37 | ); 38 | 39 | // TODO: options, etc 40 | v8::Handle argv[1] = { database }; 41 | instance = c->GetFunction()->NewInstance(1, argv); 42 | 43 | return NanEscapeScope(instance); 44 | } 45 | 46 | NAN_METHOD(Transaction::New) { 47 | NanScope(); 48 | Database *database = node::ObjectWrap::Unwrap( 49 | args[0]->ToObject() 50 | ); 51 | Transaction *transaction = new Transaction(database); 52 | transaction->t = new sophia::Transaction(database->sophia); 53 | 54 | sophia::SophiaReturnCode rc = transaction->t->Begin(); 55 | if (sophia::SOPHIA_SUCCESS != rc) { 56 | return NanThrowError(database->sophia->Error(rc)); 57 | } 58 | 59 | transaction->Wrap(args.This()); 60 | NanReturnValue(args.This()); 61 | } 62 | 63 | NAN_METHOD(Transaction::Set) { 64 | NanScope(); 65 | 66 | if (2 > args.Length() || !args[0]->IsString() || !args[1]->IsString()) { 67 | return NanThrowError("key/value required"); 68 | } 69 | 70 | char *key, *value; 71 | SP_V8_STRING_TO_CHAR_ARRAY(key, args[0]); 72 | SP_V8_STRING_TO_CHAR_ARRAY(value, args[1]); 73 | Transaction *transaction = node::ObjectWrap::Unwrap( 74 | args.This() 75 | ); 76 | sophia::SophiaReturnCode rc = transaction->t->Set(key, value); 77 | delete[] key; 78 | delete[] value; 79 | if (sophia::SOPHIA_SUCCESS != rc) { 80 | return NanThrowError( 81 | transaction->database->sophia->Error(rc) 82 | ); 83 | } 84 | 85 | NanReturnUndefined(); 86 | } 87 | 88 | NAN_METHOD(Transaction::Delete) { 89 | NanScope(); 90 | if (0 == args.Length() || !args[0]->IsString()) { 91 | return NanThrowError("key required"); 92 | } 93 | 94 | char *key; 95 | SP_V8_STRING_TO_CHAR_ARRAY(key, args[0]); 96 | Transaction *transaction = node::ObjectWrap::Unwrap( 97 | args.This() 98 | ); 99 | sophia::SophiaReturnCode rc = transaction->t->Delete(key); 100 | delete[] key; 101 | if (sophia::SOPHIA_SUCCESS != rc) { 102 | return NanThrowError( 103 | transaction->database->sophia->Error(rc) 104 | ); 105 | } 106 | NanReturnUndefined(); 107 | } 108 | 109 | NAN_METHOD(Transaction::Commit) { 110 | NanScope(); 111 | if (0 == args.Length() || !args[0]->IsFunction()) { 112 | return NanThrowError("callback required"); 113 | } 114 | v8::Local callback = args[0].As(); 115 | Transaction *transaction = node::ObjectWrap::Unwrap( 116 | args.This() 117 | ); 118 | CommitWorker *worker = new CommitWorker( 119 | transaction 120 | , new NanCallback(callback) 121 | ); 122 | // persist to prevent accidental GC 123 | v8::Local _this = args.This(); 124 | worker->SaveToPersistent("transaction", _this); 125 | NanAsyncQueueWorker(worker); 126 | NanReturnUndefined(); 127 | } 128 | 129 | NAN_METHOD(Transaction::Rollback) { 130 | NanScope(); 131 | if (0 == args.Length() || !args[0]->IsFunction()) { 132 | return NanThrowError("callback required"); 133 | } 134 | v8::Local callback = args[0].As(); 135 | Transaction *transaction = node::ObjectWrap::Unwrap( 136 | args.This() 137 | ); 138 | RollbackWorker *worker = new RollbackWorker( 139 | transaction 140 | , new NanCallback(callback) 141 | ); 142 | // persist to prevent accidental GC 143 | v8::Local _this = args.This(); 144 | worker->SaveToPersistent("transaction", _this); 145 | NanAsyncQueueWorker(worker); 146 | NanReturnUndefined(); 147 | } 148 | 149 | } // namespace sophist 150 | -------------------------------------------------------------------------------- /src/transaction.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef SOPHIST_TRANSACTION_H 3 | #define SOPHIST_TRANSACTION_H 1 4 | 5 | #include "database.h" 6 | 7 | namespace sophist { 8 | 9 | class Database; 10 | 11 | class Transaction : public node::ObjectWrap { 12 | public: 13 | Transaction(Database *database); 14 | ~Transaction(); 15 | static void Init(); 16 | static v8::Local NewInstance(v8::Local database); 17 | 18 | Database *database; 19 | sophia::Transaction *t; 20 | uint32_t id; 21 | 22 | private: 23 | static NAN_METHOD(New); 24 | static NAN_METHOD(Set); 25 | static NAN_METHOD(Delete); 26 | static NAN_METHOD(Commit); 27 | static NAN_METHOD(Rollback); 28 | }; 29 | 30 | 31 | } // namespace sophist 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /src/transaction_workers.cc: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include "sophia-cc.h" 4 | #include "transaction_workers.h" 5 | 6 | using namespace sophia; 7 | 8 | namespace sophist { 9 | 10 | CommitWorker::CommitWorker( 11 | Transaction *transaction 12 | , NanCallback *callback 13 | ) : TransactionWorker(transaction, callback) {} 14 | 15 | void CommitWorker::Execute() { 16 | SophiaReturnCode rc = transaction->t->Commit(); 17 | transaction->database->transaction = NULL; 18 | if (SOPHIA_SUCCESS != rc) { 19 | SetErrorMessage(transaction->database->sophia->Error(rc)); 20 | } 21 | } 22 | 23 | RollbackWorker::RollbackWorker( 24 | Transaction *transaction 25 | , NanCallback *callback 26 | ) : TransactionWorker(transaction, callback) {} 27 | 28 | void RollbackWorker::Execute() { 29 | SophiaReturnCode rc = transaction->t->Rollback(); 30 | transaction->database->transaction = NULL; 31 | if (SOPHIA_SUCCESS != rc) { 32 | SetErrorMessage(transaction->database->sophia->Error(rc)); 33 | } 34 | } 35 | 36 | } // namespace sophist 37 | -------------------------------------------------------------------------------- /src/transaction_workers.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef SOPHIST_TRANSACTION_WORKERS_H 3 | #define SOPHIST_TRANSACTION_WORKERS_H 1 4 | 5 | #include 6 | #include "sophia-cc.h" 7 | #include "transaction.h" 8 | 9 | namespace sophist { 10 | 11 | /** 12 | * Abstract Transaction worker. 13 | */ 14 | 15 | class TransactionWorker : public NanAsyncWorker { 16 | public: 17 | TransactionWorker( 18 | Transaction *transaction 19 | , NanCallback *callback 20 | ) : NanAsyncWorker(callback), transaction(transaction) {} 21 | 22 | protected: 23 | Transaction *transaction; 24 | }; 25 | 26 | class CommitWorker : public TransactionWorker { 27 | public: 28 | CommitWorker( 29 | Transaction *transaction 30 | , NanCallback *callback 31 | ); 32 | virtual void Execute(); 33 | }; 34 | 35 | class RollbackWorker : public TransactionWorker { 36 | public: 37 | RollbackWorker( 38 | Transaction *transaction 39 | , NanCallback *callback 40 | ); 41 | virtual void Execute(); 42 | }; 43 | 44 | } // namespace sophist 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | var rmrf = require('rimraf'); 4 | var fs = require('fs'); 5 | var exists = fs.existsSync; 6 | var Sophist = require('..'); 7 | 8 | var TEST_DB = './test-db'; 9 | 10 | describe('Sophist', function () { 11 | var db; 12 | 13 | beforeEach(cleanup); 14 | beforeEach(function* () { 15 | db = new Sophist(TEST_DB); 16 | yield db.open(); 17 | }); 18 | 19 | afterEach(function* () { 20 | yield db.close(); 21 | }); 22 | 23 | describe('#open([fn])', function () { 24 | var db; 25 | var path = TEST_DB + '_'; 26 | 27 | beforeEach(function () { 28 | db = new Sophist(path); 29 | }); 30 | 31 | afterEach(function* () { 32 | yield db.close(); 33 | }); 34 | 35 | afterEach(function (done) { 36 | rmrf(path, done); 37 | }); 38 | 39 | it('should be yieldable', function* () { 40 | yield db.open(); 41 | assert(exists(path)); 42 | }); 43 | 44 | it('should open create a new database', function (done) { 45 | db.open(function (err) { 46 | if (err) return done(err); 47 | assert(exists(path)); 48 | done(); 49 | }); 50 | }); 51 | 52 | describe('with createIfMissing: false', function () { 53 | it('should fail to create a new db', function* () { 54 | var err; 55 | try { 56 | yield db.open({ createIfMissing: false }); 57 | } catch (e) { 58 | err = e; 59 | } 60 | assert(err); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('#openSync()', function () { 66 | var db; 67 | var path = TEST_DB + '_'; 68 | 69 | beforeEach(function () { 70 | db = new Sophist(path); 71 | }); 72 | 73 | afterEach(function* () { 74 | yield db.close(); 75 | }); 76 | 77 | afterEach(function (done) { 78 | rmrf(path, done); 79 | }); 80 | 81 | it('should be yieldable', function* () { 82 | yield db.open(); 83 | assert(exists(path)); 84 | }); 85 | 86 | it('should open create a new database', function () { 87 | db.openSync(); 88 | assert(exists(TEST_DB)); 89 | }); 90 | 91 | describe('with createIfMissing: false', function () { 92 | it('should fail to create a new db', function () { 93 | var err; 94 | try { 95 | db.openSync({ createIfMissing: false }); 96 | } catch (e) { 97 | err = e; 98 | } 99 | assert(err); 100 | }); 101 | }); 102 | }); 103 | 104 | describe('#close([fn])', function () { 105 | beforeEach(function* () { 106 | yield db.close(); 107 | }); 108 | 109 | it('should be yieldable', function* () { 110 | yield db.open(); 111 | yield db.close(); 112 | var err; 113 | try { 114 | yield db.set('foo', 'bar'); 115 | } catch (e) { 116 | err = e; 117 | } 118 | assert(err && 'Database not open' == err.message); 119 | }); 120 | 121 | it('should close an opened database', function (done) { 122 | db.open(function (err) { 123 | if (err) return done(err); 124 | db.close(function (err) { 125 | if (err) return done(err); 126 | db.set('foo', 'bar', function (err) { 127 | assert(err && 'Database not open' == err.message); 128 | done(); 129 | }); 130 | }); 131 | }); 132 | }); 133 | }); 134 | 135 | describe('#closeSync', function () { 136 | beforeEach(function* () { 137 | yield db.close(); 138 | }); 139 | 140 | it('should close an opened database', function () { 141 | var err; 142 | db.openSync(); 143 | db.closeSync(); 144 | try { 145 | db.setSync('foo', 'bar'); 146 | } catch (e) { 147 | err = e; 148 | } 149 | assert(err && 'Database not open' == err.message); 150 | }); 151 | }); 152 | 153 | describe('#set(key, value, [fn])', function () { 154 | it('should set a key', function* () { 155 | yield db.set('foo', 'bar'); 156 | var value = yield db.get('foo'); 157 | assert('bar' == value); 158 | }); 159 | 160 | describe('given a callback', function () { 161 | it('should work', function (done) { 162 | db.set('foo', 'bar', function (err) { 163 | if (err) return done(err); 164 | db.get('foo', function (err, value) { 165 | if (err) return done(err); 166 | assert('bar' == value); 167 | done(); 168 | }); 169 | }); 170 | }); 171 | }); 172 | }); 173 | 174 | describe('#setSync(key, value)', function () { 175 | it('should set a key', function () { 176 | db.setSync('foo', 'bar'); 177 | assert('bar' == db.getSync('foo')); 178 | }); 179 | }); 180 | 181 | describe('#get(key, [fn])', function () { 182 | beforeEach(function* () { 183 | yield db.set('key1', 'value1'); 184 | yield db.set('key2', 'value2'); 185 | }); 186 | 187 | it('should return the value of the key', function* () { 188 | var value = yield db.get('key1'); 189 | assert('value1' == value); 190 | value = yield db.get('key2'); 191 | assert('value2' == value); 192 | }); 193 | 194 | describe('if key is missing', function () { 195 | it('should return null', function* () { 196 | var val = yield db.get('key3'); 197 | assert(null == val); 198 | }); 199 | }); 200 | 201 | describe('given a callback', function () { 202 | it('should work', function (done) { 203 | db.get('key1', function (err, value) { 204 | if (err) return done(err); 205 | assert('value1' == value); 206 | db.get('key2', function (err, value) { 207 | if (err) return done(err); 208 | assert('value2' == value); 209 | done(); 210 | }); 211 | }); 212 | }); 213 | 214 | it('should handle concurrent access', function (done) { 215 | var times = 1000; 216 | for (var i = 0; i < times; i++) 217 | db.get('key1', then); 218 | 219 | function then(err, value) { 220 | if (err) return done(err); 221 | assert('value1' == value); 222 | if (!--times) done(); 223 | } 224 | }); 225 | 226 | describe('if key is missing', function () { 227 | it('should still work', function (done) { 228 | db.get('key3', function (err, value) { 229 | if (err) return done(err); 230 | assert(null == value); 231 | done(); 232 | }); 233 | }); 234 | }); 235 | }); 236 | }); 237 | 238 | describe('#getSync(key)', function () { 239 | beforeEach(function () { 240 | db.setSync('key1', 'value1'); 241 | db.setSync('key2', 'value2'); 242 | }) 243 | 244 | it('should return the value of key', function () { 245 | assert('value1' == db.getSync('key1')); 246 | assert('value2' == db.getSync('key2')); 247 | }); 248 | 249 | describe('if key is missing', function () { 250 | it('should return null', function () { 251 | assert(null == db.getSync('key3')); 252 | }); 253 | }); 254 | }); 255 | 256 | describe('#delete(key, [fn])', function () { 257 | beforeEach(function* () { 258 | yield db.set('key1', 'value1'); 259 | yield db.set('key2', 'value2'); 260 | }); 261 | 262 | it('should remove key', function* () { 263 | yield db.delete('key1'); 264 | var val = yield db.get('key1'); 265 | assert(null == val); 266 | val = yield db.get('key2'); 267 | assert('value2' == val); 268 | }); 269 | 270 | describe('given a callback', function () { 271 | it('should work', function (done) { 272 | db.delete('key1', function (err) { 273 | if (err) return done(err); 274 | db.get('key1', function (err, value) { 275 | if (err) return done(err); 276 | assert(null == value); 277 | db.get('key2', function (err, value) { 278 | if (err) return done(err); 279 | assert('value2' == value); 280 | done(); 281 | }); 282 | }); 283 | }); 284 | }); 285 | }); 286 | }); 287 | 288 | describe('#deleteSync(key)', function () { 289 | beforeEach(function () { 290 | db.setSync('key1', 'value1'); 291 | db.setSync('key2', 'value2'); 292 | }) 293 | 294 | it('should remove key', function () { 295 | db.deleteSync('key1'); 296 | assert(null == db.getSync('key1')); 297 | assert('value2' == db.getSync('key2')); 298 | }); 299 | }); 300 | 301 | describe('#iterator()', function () { 302 | var COUNT = 10; 303 | 304 | beforeEach(function () { 305 | for (var i = 0; i < COUNT; i++) 306 | db.setSync('key' + i, 'value' + i); 307 | }); 308 | 309 | it('should create an iterator', function (done) { 310 | var it = db.iterator(); 311 | assert(it); 312 | it.end(done); 313 | }); 314 | 315 | it('should handle multiple iterators', function (done) { 316 | var n = 1000; 317 | for (var i = 0; i < n; i++) { 318 | var iterator = db.iterator(); 319 | iterator.next(factory(iterator)); 320 | } 321 | 322 | function factory(iterator) { 323 | return function next(err, key) { 324 | if (err) return done(err); 325 | assert('key0' == key); 326 | iterator.end(function (err) { 327 | if (err) return done(err); 328 | if (!--n) done(); 329 | }); 330 | }; 331 | } 332 | }); 333 | 334 | describe('with reverse: true', function () { 335 | it('should iterate in reverse', function* () { 336 | var iterator = db.iterator({ reverse: true }); 337 | var set = yield iterator.next(); 338 | assert.equal('key' + (COUNT - 1), set[0]); 339 | assert.equal('value' + (COUNT - 1), set[1]); 340 | yield iterator.end(); 341 | }); 342 | 343 | describe('with start: ', function() { 344 | it('should start at the prior key', function*(){ 345 | var iterator = db.iterator({ 346 | reverse: true, 347 | start: 'key3', 348 | }); 349 | var set = yield iterator.next(); 350 | assert.equal('key2', set[0]); 351 | assert.equal('value2', set[1]); 352 | yield iterator.end(); 353 | }); 354 | 355 | describe('with lte: true', function() { 356 | it('should start at the key', function*(){ 357 | var iterator = db.iterator({ 358 | reverse: true, 359 | lte: true, 360 | start: 'key3', 361 | }); 362 | var set = yield iterator.next(); 363 | assert.equal('key3', set[0]); 364 | assert.equal('value3', set[1]); 365 | yield iterator.end(); 366 | }); 367 | }); 368 | }); 369 | }); 370 | 371 | describe('with start: ', function () { 372 | it('should get the key/value after ', function* () { 373 | var iterator = db.iterator({ start: 'key2' }); 374 | var set = yield iterator.next(); 375 | assert.equal('key3', set[0]); 376 | assert.equal('value3', set[1]); 377 | yield iterator.end(); 378 | }); 379 | 380 | describe('with gte: true', function () { 381 | it('should get ', function* () { 382 | var iterator = db.iterator({ start: 'key2', gte: true }); 383 | var set = yield iterator.next(); 384 | assert.equal('key2', set[0]); 385 | assert.equal('value2', set[1]); 386 | yield iterator.end(); 387 | }); 388 | }); 389 | 390 | describe('if is not a string', function () { 391 | it('should throw', function () { 392 | var err; 393 | try { 394 | db.iterator({ start: ['a', 'b', 'c'] }); 395 | } catch (e) { 396 | err = e; 397 | } 398 | assert(err && 'start key must be a string' == err.message); 399 | }); 400 | }); 401 | }); 402 | 403 | describe('with end: ', function () { 404 | it('should stop the iterator at ', function* () { 405 | var iterator = db.iterator({ end: 'key2' }); 406 | var set = yield iterator.next(); 407 | assert.equal('key0', set[0]); 408 | set = yield iterator.next(); 409 | assert.equal('key1', set[0]); 410 | assert(null == (yield iterator.next())); 411 | yield iterator.end(); 412 | }); 413 | 414 | describe('if is not a string', function () { 415 | it('should throw', function () { 416 | var err; 417 | try { 418 | db.iterator({ end: ['a', 'b', 'c'] }); 419 | } catch (e) { 420 | err = e; 421 | } 422 | assert(err && 'end key must be a string' == err.message); 423 | }); 424 | }); 425 | }); 426 | 427 | describe('with reverse, start, end, and gte', function () { 428 | it('should work', function* () { 429 | var iterator = db.iterator({ 430 | reverse: true, 431 | start: 'key3', 432 | end: 'key1', 433 | gte: true, 434 | }); 435 | assert.equal('key2', (yield iterator.next())[0]) 436 | assert.equal(null, (yield iterator.next())); 437 | yield iterator.end(); 438 | }); 439 | }); 440 | 441 | describe('#next([fn])', function () { 442 | var iterator; 443 | 444 | beforeEach(function () { 445 | iterator = db.iterator(); 446 | }); 447 | 448 | afterEach(function (done) { 449 | iterator.end(done); 450 | }); 451 | 452 | it('should be yieldable', function* () { 453 | var arr = yield iterator.next(); 454 | assert('key0' == arr[0]); 455 | assert('value0' == arr[1]); 456 | }); 457 | 458 | it('should get the next key', function (done) { 459 | iterator.next(function (err, key, value) { 460 | if (err) return done(err); 461 | assert('key0' == key); 462 | assert('value0' == value); 463 | done(); 464 | }); 465 | }); 466 | 467 | describe('the last key', function () { 468 | beforeEach(function (done) { 469 | var pending = COUNT; 470 | iterator.next(next); 471 | function next(err) { 472 | if (err) return done(err); 473 | if (!--pending) return done(); 474 | iterator.next(next); 475 | } 476 | }); 477 | 478 | it('should provide nulls', function (done) { 479 | iterator.next(function (err, key, value) { 480 | if (err) return done(err); 481 | assert(null == key); 482 | assert(null == value); 483 | done(); 484 | }); 485 | }); 486 | 487 | it('should be provide null when yielded', function* () { 488 | var arr = yield iterator.next(); 489 | assert(null == arr); 490 | }); 491 | }); 492 | }); 493 | 494 | describe('#nextSync()', function () { 495 | var iterator; 496 | 497 | beforeEach(function () { 498 | iterator = db.iterator(); 499 | }); 500 | afterEach(function (done) { 501 | iterator.end(done); 502 | }); 503 | 504 | it('should return [key, value]', function () { 505 | var arr = iterator.nextSync(); 506 | assert.equal('key0', arr[0]); 507 | assert.equal('value0', arr[1]); 508 | }); 509 | }); 510 | 511 | describe('#end([fn])', function () { 512 | it('should be yieldable', function* () { 513 | var iterator = db.iterator(); 514 | yield iterator.end(); 515 | }); 516 | 517 | it('should end the iterator', function (done) { 518 | var iterator = db.iterator(); 519 | iterator.end(done); 520 | }); 521 | }); 522 | 523 | describe('#endSync()', function () { 524 | it('should end the iterator', function () { 525 | var iterator = db.iterator(); 526 | iterator.endSync(); 527 | }); 528 | }); 529 | }); 530 | 531 | describe('#transaction()', function () { 532 | it('should create a transaction', function () { 533 | var transaction = db.transaction(); 534 | assert('Transaction' == transaction.constructor.name); 535 | }); 536 | 537 | describe('with another transaction open', function () { 538 | it('should throw', function* () { 539 | var err; 540 | var t1 = db.transaction(); 541 | try { 542 | db.transaction(); 543 | } catch (e) { 544 | err = e; 545 | } 546 | assert(err && ~err.message.indexOf('transaction is already open')); 547 | yield t1.rollback(); 548 | }); 549 | }); 550 | 551 | describe('#set(key, value)', function () { 552 | it('should add a pending set', function* () { 553 | var transaction = db.transaction(); 554 | transaction.set('foo', 'bar'); 555 | assert(null == (yield db.get('foo'))); 556 | yield transaction.commit(); 557 | assert('bar' == (yield db.get('foo'))); 558 | }); 559 | }); 560 | 561 | describe('#delete(key)', function () { 562 | it('should add a pending delete', function* () { 563 | yield db.set('foo', 'bar'); 564 | var transaction = db.transaction(); 565 | transaction.delete('foo'); 566 | assert('bar' == (yield db.get('foo'))); 567 | yield transaction.commit(); 568 | assert(null == (yield db.get('foo'))); 569 | }); 570 | }); 571 | 572 | describe('#commit([fn])', function () { 573 | var transaction; 574 | 575 | beforeEach(function () { 576 | transaction = db.transaction(); 577 | for (var i = 0; i < 100; i++) transaction.set(String(i), String(i)); 578 | }); 579 | 580 | it('should commit the pending writes', function (done) { 581 | transaction.commit(done); 582 | }); 583 | 584 | it('should be yieldable', function* () { 585 | yield transaction.commit(); 586 | assert('40' == (yield db.get('40'))); 587 | }); 588 | }); 589 | 590 | describe('#rollback([fn])', function () { 591 | var transaction; 592 | 593 | beforeEach(function () { 594 | transaction = db.transaction(); 595 | for (var i = 0; i < 100; i++) transaction.set(String(i), String(i)); 596 | }); 597 | 598 | it('should rollback the pending writes', function (done) { 599 | transaction.rollback(done); 600 | }); 601 | 602 | it('should be yieldable', function* () { 603 | yield transaction.rollback(); 604 | assert(null == (yield db.get('40'))); 605 | }); 606 | }); 607 | }); 608 | }); 609 | 610 | 611 | function cleanup(done) { 612 | rmrf(TEST_DB, done); 613 | } 614 | --------------------------------------------------------------------------------