From bafce61979de3e7ae4a5ada9f4e8a13a5b93b542 Mon Sep 17 00:00:00 2001 From: Saghm Rossi Date: Tue, 4 Nov 2014 22:05:17 -0500 Subject: [PATCH] first rdb commit Summary: First commit for rdb shell Test Plan: unit_test.js does simple assertions on most of the main functionality; will update with rest of tests Reviewers: igor, rven, lijn, yhciang, sdong Subscribers: dhruba, leveldb Differential Revision: https://reviews.facebook.net/D28749 --- tools/rdb/.gitignore | 1 + tools/rdb/API.md | 178 ++++++++++++++ tools/rdb/README.md | 40 +++ tools/rdb/binding.gyp | 25 ++ tools/rdb/db_wrapper.cc | 525 ++++++++++++++++++++++++++++++++++++++++ tools/rdb/db_wrapper.h | 58 +++++ tools/rdb/rdb | 3 + tools/rdb/rdb.cc | 15 ++ tools/rdb/unit_test.js | 124 ++++++++++ 9 files changed, 969 insertions(+) create mode 100644 tools/rdb/.gitignore create mode 100644 tools/rdb/API.md create mode 100644 tools/rdb/README.md create mode 100644 tools/rdb/binding.gyp create mode 100644 tools/rdb/db_wrapper.cc create mode 100644 tools/rdb/db_wrapper.h create mode 100755 tools/rdb/rdb create mode 100644 tools/rdb/rdb.cc create mode 100644 tools/rdb/unit_test.js diff --git a/tools/rdb/.gitignore b/tools/rdb/.gitignore new file mode 100644 index 000000000..378eac25d --- /dev/null +++ b/tools/rdb/.gitignore @@ -0,0 +1 @@ +build diff --git a/tools/rdb/API.md b/tools/rdb/API.md new file mode 100644 index 000000000..f25949706 --- /dev/null +++ b/tools/rdb/API.md @@ -0,0 +1,178 @@ +# JavaScript API + +## DBWrapper + +### Constructor + + # Creates a new database wrapper object + RDB() + +### Open + + # Open a new or existing RocksDB database. + # + # db_name (string) - Location of the database (inside the + # `/tmp` directory). + # column_families (string[]) - Names of additional column families + # beyond the default. If there are no other + # column families, this argument can be + # left off. + # + # Returns true if the database was opened successfully, or false otherwise + db_obj.(db_name, column_families = []) + +### Get + + # Get the value of a given key. + # + # key (string) - Which key to get the value of. + # column_family (string) - Which column family to check for the key. + # This argument can be left off for the default + # column family + # + # Returns the value (string) that is associated with the given key if + # one exists, or null otherwise. + db_obj.get(key, column_family = { default }) + +### Put + + # Associate a value with a key. + # + # key (string) - Which key to associate the value with. + # value (string) - The value to associate with the key. + # column_family (string) - Which column family to put the key-value pair + # in. This argument can be left off for the + # default column family. + # + # Returns true if the key-value pair was successfully stored in the + # database, or false otherwise. + db_obj.put(key, value, column_family = { default }) + +### Delete + + # Delete a value associated with a given key. + # + # key (string) - Which key to delete the value of.. + # column_family (string) - Which column family to check for the key. + # This argument can be left off for the default + # column family + # + # Returns true if an error occured while trying to delete the key in + # the database, or false otherwise. Note that this is NOT the same as + # whether a value was deleted; in the case of a specified key not having + # a value, this will still return true. Use the `get` method prior to + # this method to check if a value existed before the call to `delete`. + db_obj.delete(key, column_family = { default }) + +### Dump + + # Print out all the key-value pairs in a given column family of the + # database. + # + # column_family (string) - Which column family to dump the pairs from. + # This argument can be left off for the default + # column family. + # + # Returns true if the keys were successfully read from the database, or + # false otherwise. + db_obj.dump(column_family = { default }) + +### WriteBatch + + # Execute an atomic batch of writes (i.e. puts and deletes) to the + # database. + # + # cf_batches (BatchObject[]; see below) - Put and Delete writes grouped + # by column family to execute + # atomically. + # + # Returns true if the argument array was well-formed and was + # successfully written to the database, or false otherwise. + db_obj.writeBatch(cf_batches) + +### CreateColumnFamily + + # Create a new column familiy for the database. + # + # column_family_name (string) - Name of the new column family. + # + # Returns true if the new column family was successfully created, or + # false otherwise. + db_obj.createColumnFamily(column_family_name) + +### CompactRange + + # Compact the underlying storage for a given range. + # + # In addition to the endpoints of the range, the method is overloaded to + # accept a non-default column family, a set of options, or both. + # + # begin (string) - First key in the range to compact. + # end (string) - Last key in the range to compact. + # options (object) - Contains a subset of the following key-value + # pairs: + # * 'target_level' => int + # * 'target_path_id' => int + # column_family (string) - Which column family to compact the range in. + db_obj.compactRange(begin, end) + db_obj.compactRange(begin, end, options) + db_obj.compactRange(begin, end, column_family) + db_obj.compactRange(begin, end, options, column_family) + + + +### Close + + # Close an a database and free the memory associated with it. + # + # Return null. + # db_obj.close() + + +## BatchObject + +### Structure + +A BatchObject must have at least one of the following key-value pairs: + +* 'put' => Array of ['string1', 'string1'] pairs, each of which signifies that +the key 'string1' should be associated with the value 'string2' +* 'delete' => Array of strings, each of which is a key whose value should be +deleted. + +The following key-value pair is optional: + +* 'column_family' => The name (string) of the column family to apply the +changes to. + +### Examples + + # Writes the key-value pairs 'firstname' => 'Saghm' and + # 'lastname' => 'Rossi' atomically to the database. + db_obj.writeBatch([ + { + put: [ ['firstname', 'Saghm'], ['lastname', 'Rossi'] ] + } + ]); + + + # Deletes the values associated with 'firstname' and 'lastname' in + # the default column family and adds the key 'number_of_people' with + # with the value '2'. Additionally, adds the key-value pair + # 'name' => 'Saghm Rossi' to the column family 'user1' and the pair + # 'name' => 'Matt Blaze' to the column family 'user2'. All writes + # are done atomically. + db_obj.writeBatch([ + { + put: [ ['number_of_people', '2'] ], + delete: ['firstname', 'lastname'] + }, + { + put: [ ['name', 'Saghm Rossi'] ], + column_family: 'user1' + }, + { + put: [ ['name', Matt Blaze'] ], + column_family: 'user2' + } + ]); diff --git a/tools/rdb/README.md b/tools/rdb/README.md new file mode 100644 index 000000000..2cc9acad2 --- /dev/null +++ b/tools/rdb/README.md @@ -0,0 +1,40 @@ +# RDB - RocksDB Shell + +RDB is a NodeJS-based shell interface to RocksDB. It can also be used as a +JavaScript binding for RocksDB within a Node application. + +## Setup/Compilation + +### Requirements + +* static RocksDB library (i.e. librocksdb.a) +* libsnappy +* node (tested onv0.10.33, no guarantees on anything else!) +* node-gyp +* python2 (for node-gyp; tested with 2.7.8) + +### Installation + +NOTE: If your default `python` binary is not a version of python2, add +the arguments `--python /path/to/python2` to the the `node-gyp` commands. + +1. Make sure you have the static library (i.e. "librocksdb.a") in the root +directory of your rocksdb installation. If not, `cd` there and run +`make static_lib`. + +2. Run `node-gyp configure` to generate the build. + +3. Run `node-gyp build` to compile RDB. + +## Usage + +### Running the shell + +Assuming everything compiled correctly, you can run the `rdb` executable +located in the root of the `tools/rdb` directory to start the shell. The file is +just a shell script that runs the node shell and loads the constructor for the +RDB object into the top-level function `RDB`. + +### JavaScript API + +See `API.md` for how to use RocksDB from the shell. diff --git a/tools/rdb/binding.gyp b/tools/rdb/binding.gyp new file mode 100644 index 000000000..89145541c --- /dev/null +++ b/tools/rdb/binding.gyp @@ -0,0 +1,25 @@ +{ + "targets": [ + { + "target_name": "rdb", + "sources": [ + "rdb.cc", + "db_wrapper.cc", + "db_wrapper.h" + ], + "cflags_cc!": [ + "-fno-exceptions" + ], + "cflags_cc+": [ + "-std=c++11", + ], + "include_dirs+": [ + "../../include" + ], + "libraries": [ + "../../../librocksdb.a", + "-lsnappy" + ], + } + ] +} diff --git a/tools/rdb/db_wrapper.cc b/tools/rdb/db_wrapper.cc new file mode 100644 index 000000000..34725379d --- /dev/null +++ b/tools/rdb/db_wrapper.cc @@ -0,0 +1,525 @@ +#include +#include +#include +#include +#include + +#include "db_wrapper.h" +#include "rocksdb/db.h" +#include "rocksdb/slice.h" +#include "rocksdb/options.h" + +namespace { + void printWithBackSlashes(std::string str) { + for (std::string::size_type i = 0; i < str.size(); i++) { + if (str[i] == '\\' || str[i] == '"') { + std::cout << "\\"; + } + + std::cout << str[i]; + } + } + + bool has_key_for_array(Local obj, std::string key) { + return obj->Has(String::NewSymbol(key.c_str())) && + obj->Get(String::NewSymbol(key.c_str()))->IsArray(); + } +} + +using namespace v8; + + +Persistent DBWrapper::constructor; + +DBWrapper::DBWrapper() { + options_.IncreaseParallelism(); + options_.OptimizeLevelStyleCompaction(); + options_.disable_auto_compactions = true; + options_.create_if_missing = true; +} + +DBWrapper::~DBWrapper() { + delete db_; +} + +bool DBWrapper::HasFamilyNamed(std::string& name, DBWrapper* db) { + return db->columnFamilies_.find(name) != db->columnFamilies_.end(); +} + + +void DBWrapper::Init(Handle exports) { + Local tpl = FunctionTemplate::New(New); + tpl->SetClassName(String::NewSymbol("DBWrapper")); + tpl->InstanceTemplate()->SetInternalFieldCount(8); + tpl->PrototypeTemplate()->Set(String::NewSymbol("open"), + FunctionTemplate::New(Open)->GetFunction()); + tpl->PrototypeTemplate()->Set(String::NewSymbol("get"), + FunctionTemplate::New(Get)->GetFunction()); + tpl->PrototypeTemplate()->Set(String::NewSymbol("put"), + FunctionTemplate::New(Put)->GetFunction()); + tpl->PrototypeTemplate()->Set(String::NewSymbol("delete"), + FunctionTemplate::New(Delete)->GetFunction()); + tpl->PrototypeTemplate()->Set(String::NewSymbol("dump"), + FunctionTemplate::New(Dump)->GetFunction()); + tpl->PrototypeTemplate()->Set(String::NewSymbol("createColumnFamily"), + FunctionTemplate::New(CreateColumnFamily)->GetFunction()); + tpl->PrototypeTemplate()->Set(String::NewSymbol("writeBatch"), + FunctionTemplate::New(WriteBatch)->GetFunction()); + tpl->PrototypeTemplate()->Set(String::NewSymbol("compactRange"), + FunctionTemplate::New(CompactRange)->GetFunction()); + + constructor = Persistent::New(tpl->GetFunction()); + exports->Set(String::NewSymbol("DBWrapper"), constructor); +} + +Handle DBWrapper::Open(const Arguments& args) { + HandleScope scope; + DBWrapper* db_wrapper = ObjectWrap::Unwrap(args.This()); + + if (!(args[0]->IsString() && + (args[1]->IsUndefined() || args[1]->IsArray()))) { + return scope.Close(Boolean::New(false)); + } + + std::string db_file = *v8::String::Utf8Value(args[0]->ToString()); + + std::vector cfs = { rocksdb::kDefaultColumnFamilyName }; + + if (!args[1]->IsUndefined()) { + Handle array = Handle::Cast(args[1]); + for (uint i = 0; i < array->Length(); i++) { + if (!array->Get(i)->IsString()) { + return scope.Close(Boolean::New(false)); + } + + cfs.push_back(*v8::String::Utf8Value(array->Get(i)->ToString())); + } + } + + if (cfs.size() == 1) { + db_wrapper->status_ = rocksdb::DB::Open( + db_wrapper->options_, db_file, &db_wrapper->db_); + + return scope.Close(Boolean::New(db_wrapper->status_.ok())); + } + + std::vector families; + + for (std::vector::size_type i = 0; i < cfs.size(); i++) { + families.push_back(rocksdb::ColumnFamilyDescriptor( + cfs[i], rocksdb::ColumnFamilyOptions())); + } + + std::vector handles; + db_wrapper->status_ = rocksdb::DB::Open( + db_wrapper->options_, db_file, families, &handles, &db_wrapper->db_); + + if (!db_wrapper->status_.ok()) { + return scope.Close(Boolean::New(db_wrapper->status_.ok())); + } + + for (std::vector::size_type i = 0; i < handles.size(); i++) { + db_wrapper->columnFamilies_[cfs[i]] = handles[i]; + } + + return scope.Close(Boolean::New(true)); +} + + +Handle DBWrapper::New(const Arguments& args) { + HandleScope scope; + Handle to_return; + + if (args.IsConstructCall()) { + DBWrapper* db_wrapper = new DBWrapper(); + db_wrapper->Wrap(args.This()); + + return args.This(); + } + + const int argc = 0; + Local argv[0] = {}; + + return scope.Close(constructor->NewInstance(argc, argv)); +} + +Handle DBWrapper::Get(const Arguments& args) { + HandleScope scope; + + if (!(args[0]->IsString() && + (args[1]->IsUndefined() || args[1]->IsString()))) { + return scope.Close(Null()); + } + + DBWrapper* db_wrapper = ObjectWrap::Unwrap(args.This()); + std::string key = *v8::String::Utf8Value(args[0]->ToString()); + std::string cf = *v8::String::Utf8Value(args[1]->ToString()); + std::string value; + + if (args[1]->IsUndefined()) { + db_wrapper->status_ = db_wrapper->db_->Get( + rocksdb::ReadOptions(), key, &value); + } else if (db_wrapper->HasFamilyNamed(cf, db_wrapper)) { + db_wrapper->status_ = db_wrapper->db_->Get( + rocksdb::ReadOptions(), db_wrapper->columnFamilies_[cf], key, &value); + } else { + return scope.Close(Null()); + } + + Handle v = db_wrapper->status_.ok() ? + String::NewSymbol(value.c_str()) : Null(); + + return scope.Close(v); +} + +Handle DBWrapper::Put(const Arguments& args) { + HandleScope scope; + + if (!(args[0]->IsString() && args[1]->IsString() && + (args[2]->IsUndefined() || args[2]->IsString()))) { + return scope.Close(Boolean::New(false)); + } + + DBWrapper* db_wrapper = ObjectWrap::Unwrap(args.This()); + std::string key = *v8::String::Utf8Value(args[0]->ToString()); + std::string value = *v8::String::Utf8Value(args[1]->ToString()); + std::string cf = *v8::String::Utf8Value(args[2]->ToString()); + + if (args[2]->IsUndefined()) { + db_wrapper->status_ = db_wrapper->db_->Put( + rocksdb::WriteOptions(), key, value + ); + } else if (db_wrapper->HasFamilyNamed(cf, db_wrapper)) { + db_wrapper->status_ = db_wrapper->db_->Put( + rocksdb::WriteOptions(), + db_wrapper->columnFamilies_[cf], + key, + value + ); + } else { + return scope.Close(Boolean::New(false)); + } + + + return scope.Close(Boolean::New(db_wrapper->status_.ok())); +} + +Handle DBWrapper::Delete(const Arguments& args) { + HandleScope scope; + + if (!args[0]->IsString()) { + return scope.Close(Boolean::New(false)); + } + + DBWrapper* db_wrapper = ObjectWrap::Unwrap(args.This()); + std::string arg0 = *v8::String::Utf8Value(args[0]->ToString()); + std::string arg1 = *v8::String::Utf8Value(args[1]->ToString()); + + if (args[1]->IsUndefined()) { + db_wrapper->status_ = db_wrapper->db_->Delete( + rocksdb::WriteOptions(), arg0); + } else { + if (!db_wrapper->HasFamilyNamed(arg1, db_wrapper)) { + return scope.Close(Boolean::New(false)); + } + db_wrapper->status_ = db_wrapper->db_->Delete( + rocksdb::WriteOptions(), db_wrapper->columnFamilies_[arg1], arg0); + } + + return scope.Close(Boolean::New(db_wrapper->status_.ok())); +} + +Handle DBWrapper::Dump(const Arguments& args) { + HandleScope scope; + std::unique_ptr iterator; + DBWrapper* db_wrapper = ObjectWrap::Unwrap(args.This()); + std::string arg0 = *v8::String::Utf8Value(args[0]->ToString()); + + if (args[0]->IsUndefined()) { + iterator.reset(db_wrapper->db_->NewIterator(rocksdb::ReadOptions())); + } else { + if (!db_wrapper->HasFamilyNamed(arg0, db_wrapper)) { + return scope.Close(Boolean::New(false)); + } + + iterator.reset(db_wrapper->db_->NewIterator( + rocksdb::ReadOptions(), db_wrapper->columnFamilies_[arg0])); + } + + iterator->SeekToFirst(); + + while (iterator->Valid()) { + std::cout << "\""; + printWithBackSlashes(iterator->key().ToString()); + std::cout << "\" => \""; + printWithBackSlashes(iterator->value().ToString()); + std::cout << "\"\n"; + iterator->Next(); + } + + return scope.Close(Boolean::New(true)); +} + +Handle DBWrapper::CreateColumnFamily(const Arguments& args) { + HandleScope scope; + + if (!args[0]->IsString()) { + return scope.Close(Boolean::New(false)); + } + + DBWrapper* db_wrapper = ObjectWrap::Unwrap(args.This()); + std::string cf_name = *v8::String::Utf8Value(args[0]->ToString()); + + if (db_wrapper->HasFamilyNamed(cf_name, db_wrapper)) { + return scope.Close(Boolean::New(false)); + } + + rocksdb::ColumnFamilyHandle* cf; + db_wrapper->status_ = db_wrapper->db_->CreateColumnFamily( + rocksdb::ColumnFamilyOptions(), cf_name, &cf); + + if (!db_wrapper->status_.ok()) { + return scope.Close(Boolean::New(false)); + } + + db_wrapper->columnFamilies_[cf_name] = cf; + + return scope.Close(Boolean::New(true)); +} + +bool DBWrapper::AddToBatch(rocksdb::WriteBatch& batch, bool del, + Handle array) { + Handle put_pair; + for (uint i = 0; i < array->Length(); i++) { + if (del) { + if (!array->Get(i)->IsString()) { + return false; + } + + batch.Delete(*v8::String::Utf8Value(array->Get(i)->ToString())); + continue; + } + + if (!array->Get(i)->IsArray()) { + return false; + } + + put_pair = Handle::Cast(array->Get(i)); + + if (!put_pair->Get(0)->IsString() || !put_pair->Get(1)->IsString()) { + return false; + } + + batch.Put( + *v8::String::Utf8Value(put_pair->Get(0)->ToString()), + *v8::String::Utf8Value(put_pair->Get(1)->ToString())); + } + + return true; +} + +bool DBWrapper::AddToBatch(rocksdb::WriteBatch& batch, bool del, + Handle array, DBWrapper* db_wrapper, + std::string cf) { + Handle put_pair; + for (uint i = 0; i < array->Length(); i++) { + if (del) { + if (!array->Get(i)->IsString()) { + return false; + } + + batch.Delete( + db_wrapper->columnFamilies_[cf], + *v8::String::Utf8Value(array->Get(i)->ToString())); + continue; + } + + if (!array->Get(i)->IsArray()) { + return false; + } + + put_pair = Handle::Cast(array->Get(i)); + + if (!put_pair->Get(0)->IsString() || !put_pair->Get(1)->IsString()) { + return false; + } + + batch.Put( + db_wrapper->columnFamilies_[cf], + *v8::String::Utf8Value(put_pair->Get(0)->ToString()), + *v8::String::Utf8Value(put_pair->Get(1)->ToString())); + } + + return true; +} + +Handle DBWrapper::WriteBatch(const Arguments& args) { + HandleScope scope; + + if (!args[0]->IsArray()) { + return scope.Close(Boolean::New(false)); + } + + DBWrapper* db_wrapper = ObjectWrap::Unwrap(args.This()); + Handle sub_batches = Handle::Cast(args[0]); + Local sub_batch; + rocksdb::WriteBatch batch; + bool well_formed; + + for (uint i = 0; i < sub_batches->Length(); i++) { + if (!sub_batches->Get(i)->IsObject()) { + return scope.Close(Boolean::New(false)); + } + sub_batch = sub_batches->Get(i)->ToObject(); + + if (sub_batch->Has(String::NewSymbol("column_family"))) { + if (!has_key_for_array(sub_batch, "put") && + !has_key_for_array(sub_batch, "delete")) { + return scope.Close(Boolean::New(false)); + } + + well_formed = db_wrapper->AddToBatch( + batch, false, + Handle::Cast(sub_batch->Get(String::NewSymbol("put"))), + db_wrapper, *v8::String::Utf8Value(sub_batch->Get( + String::NewSymbol("column_family")))); + + well_formed = db_wrapper->AddToBatch( + batch, true, + Handle::Cast(sub_batch->Get(String::NewSymbol("delete"))), + db_wrapper, *v8::String::Utf8Value(sub_batch->Get( + String::NewSymbol("column_family")))); + } else { + well_formed = db_wrapper->AddToBatch( + batch, false, + Handle::Cast(sub_batch->Get(String::NewSymbol("put")))); + well_formed = db_wrapper->AddToBatch( + batch, true, + Handle::Cast(sub_batch->Get(String::NewSymbol("delete")))); + + if (!well_formed) { + return scope.Close(Boolean::New(false)); + } + } + } + + db_wrapper->status_ = db_wrapper->db_->Write(rocksdb::WriteOptions(), &batch); + + return scope.Close(Boolean::New(db_wrapper->status_.ok())); +} + +Handle DBWrapper::CompactRangeDefault(const Arguments& args) { + HandleScope scope; + + DBWrapper* db_wrapper = ObjectWrap::Unwrap(args.This()); + rocksdb::Slice begin = *v8::String::Utf8Value(args[0]->ToString()); + rocksdb::Slice end = *v8::String::Utf8Value(args[1]->ToString()); + db_wrapper->status_ = db_wrapper->db_->CompactRange(&end, &begin); + + return scope.Close(Boolean::New(db_wrapper->status_.ok())); +} + +Handle DBWrapper::CompactColumnFamily(const Arguments& args) { + HandleScope scope; + + DBWrapper* db_wrapper = ObjectWrap::Unwrap(args.This()); + rocksdb::Slice begin = *v8::String::Utf8Value(args[0]->ToString()); + rocksdb::Slice end = *v8::String::Utf8Value(args[1]->ToString()); + std::string cf = *v8::String::Utf8Value(args[2]->ToString()); + db_wrapper->status_ = db_wrapper->db_->CompactRange( + db_wrapper->columnFamilies_[cf], &begin, &end); + + return scope.Close(Boolean::New(db_wrapper->status_.ok())); +} + +Handle DBWrapper::CompactOptions(const Arguments& args) { + HandleScope scope; + + if (!args[2]->IsObject()) { + return scope.Close(Boolean::New(false)); + } + + DBWrapper* db_wrapper = ObjectWrap::Unwrap(args.This()); + rocksdb::Slice begin = *v8::String::Utf8Value(args[0]->ToString()); + rocksdb::Slice end = *v8::String::Utf8Value(args[1]->ToString()); + Local options = args[2]->ToObject(); + int target_level = -1, target_path_id = 0; + + if (options->Has(String::NewSymbol("target_level")) && + options->Get(String::NewSymbol("target_level"))->IsInt32()) { + target_level = (int)(options->Get( + String::NewSymbol("target_level"))->ToInt32()->Value()); + + if (options->Has(String::NewSymbol("target_path_id")) || + options->Get(String::NewSymbol("target_path_id"))->IsInt32()) { + target_path_id = (int)(options->Get( + String::NewSymbol("target_path_id"))->ToInt32()->Value()); + } + } + + db_wrapper->status_ = db_wrapper->db_->CompactRange( + &begin, &end, true, target_level, target_path_id + ); + + return scope.Close(Boolean::New(db_wrapper->status_.ok())); +} + +Handle DBWrapper::CompactAll(const Arguments& args) { + HandleScope scope; + + if (!args[2]->IsObject() || !args[3]->IsString()) { + return scope.Close(Boolean::New(false)); + } + + DBWrapper* db_wrapper = ObjectWrap::Unwrap(args.This()); + rocksdb::Slice begin = *v8::String::Utf8Value(args[0]->ToString()); + rocksdb::Slice end = *v8::String::Utf8Value(args[1]->ToString()); + Local options = args[2]->ToObject(); + std::string cf = *v8::String::Utf8Value(args[3]->ToString()); + + int target_level = -1, target_path_id = 0; + + if (options->Has(String::NewSymbol("target_level")) && + options->Get(String::NewSymbol("target_level"))->IsInt32()) { + target_level = (int)(options->Get( + String::NewSymbol("target_level"))->ToInt32()->Value()); + + if (options->Has(String::NewSymbol("target_path_id")) || + options->Get(String::NewSymbol("target_path_id"))->IsInt32()) { + target_path_id = (int)(options->Get( + String::NewSymbol("target_path_id"))->ToInt32()->Value()); + } + } + + db_wrapper->status_ = db_wrapper->db_->CompactRange( + db_wrapper->columnFamilies_[cf], &begin, &end, true, target_level, + target_path_id); + + return scope.Close(Boolean::New(db_wrapper->status_.ok())); +} + +Handle DBWrapper::CompactRange(const Arguments& args) { + HandleScope scope; + + if (!args[0]->IsString() || !args[1]->IsString()) { + return scope.Close(Boolean::New(false)); + } + + switch(args.Length()) { + case 2: + return CompactRangeDefault(args); + case 3: + return args[2]->IsString() ? CompactColumnFamily(args) : + CompactOptions(args); + default: + return CompactAll(args); + } +} + +Handle DBWrapper::Close(const Arguments& args) { + HandleScope scope; + + delete ObjectWrap::Unwrap(args.This()); + + return scope.Close(Null()); +} diff --git a/tools/rdb/db_wrapper.h b/tools/rdb/db_wrapper.h new file mode 100644 index 000000000..9d1c8f886 --- /dev/null +++ b/tools/rdb/db_wrapper.h @@ -0,0 +1,58 @@ +#ifndef DBWRAPPER_H +#define DBWRAPPER_H + +#include +#include + +#include "rocksdb/db.h" +#include "rocksdb/slice.h" +#include "rocksdb/options.h" + +using namespace v8; + +// Used to encapsulate a particular instance of an opened database. +// +// This object should not be used directly in C++; it exists solely to provide +// a mapping from a JavaScript object to a C++ code that can use the RocksDB +// API. +class DBWrapper : public node::ObjectWrap { + public: + static void Init(Handle exports); + + private: + explicit DBWrapper(); + ~DBWrapper(); + + // Helper methods + static bool HasFamilyNamed(std::string& name, DBWrapper* db); + static bool AddToBatch(rocksdb::WriteBatch& batch, bool del, + Handle array); + static bool AddToBatch(rocksdb::WriteBatch& batch, bool del, + Handle array, DBWrapper* db_wrapper, std::string cf); + static Handle CompactRangeDefault(const v8::Arguments& args); + static Handle CompactColumnFamily(const Arguments& args); + static Handle CompactOptions(const Arguments& args); + static Handle CompactAll(const Arguments& args); + + // C++ mappings of API methods + static Persistent constructor; + static Handle Open(const Arguments& args); + static Handle New(const Arguments& args); + static Handle Get(const Arguments& args); + static Handle Put(const Arguments& args); + static Handle Delete(const Arguments& args); + static Handle Dump(const Arguments& args); + static Handle WriteBatch(const Arguments& args); + static Handle CreateColumnFamily(const Arguments& args); + static Handle CompactRange(const Arguments& args); + static Handle Close(const Arguments& args); + + // Internal fields + rocksdb::Options options_; + rocksdb::Status status_; + rocksdb::DB* db_; + std::unordered_map + columnFamilies_; +}; + +#endif diff --git a/tools/rdb/rdb b/tools/rdb/rdb new file mode 100755 index 000000000..82cd17fb7 --- /dev/null +++ b/tools/rdb/rdb @@ -0,0 +1,3 @@ +#!/bin/bash + +node -e "RDB = require('./build/Release/rdb').DBWrapper; console.log('Loaded rocksdb in variable RDB'); repl = require('repl').start('> ');" diff --git a/tools/rdb/rdb.cc b/tools/rdb/rdb.cc new file mode 100644 index 000000000..8710e4623 --- /dev/null +++ b/tools/rdb/rdb.cc @@ -0,0 +1,15 @@ +#ifndef BUILDING_NODE_EXTENSION +#define BUILDING_NODE_EXTENSION +#endif + +#include +#include +#include "db_wrapper.h" + +using namespace v8; + +void InitAll(Handle exports) { + DBWrapper::Init(exports); +} + +NODE_MODULE(rdb, InitAll) diff --git a/tools/rdb/unit_test.js b/tools/rdb/unit_test.js new file mode 100644 index 000000000..d74ee8ce5 --- /dev/null +++ b/tools/rdb/unit_test.js @@ -0,0 +1,124 @@ +assert = require('assert') +RDB = require('./build/Release/rdb').DBWrapper +exec = require('child_process').exec +util = require('util') + +DB_NAME = '/tmp/rocksdbtest-' + process.getuid() + +a = RDB() +assert.equal(a.open(DB_NAME, ['b']), false) + +exec( + util.format( + "node -e \"RDB = require('./build/Release/rdb').DBWrapper; \ + a = RDB('%s'); a.createColumnFamily('b')\"", + DB_NAME + ).exitCode, null +) + + +exec( + util.format( + "node -e \"RDB = require('./build/Release/rdb').DBWrapper; \ + a = RDB('%s', ['b'])\"", + DB_NAME + ).exitCode, null +) + +exec('rm -rf ' + DB_NAME) + +a = RDB() +assert.equal(a.open(DB_NAME, ['a']), false) +assert(a.open(DB_NAME), true) +assert(a.createColumnFamily('temp')) + +b = RDB() +assert.equal(b.open(DB_NAME), false) + +exec('rm -rf ' + DB_NAME) + +DB_NAME += 'b' + +a = RDB() +assert(a.open(DB_NAME)) +assert.equal(a.constructor.name, 'DBWrapper') +assert.equal(a.createColumnFamily(), false) +assert.equal(a.createColumnFamily(1), false) +assert.equal(a.createColumnFamily(['']), false) +assert(a.createColumnFamily('b')) +assert.equal(a.createColumnFamily('b'), false) + +// Get and Put +assert.equal(a.get(1), null) +assert.equal(a.get(['a']), null) +assert.equal(a.get('a', 1), null) +assert.equal(a.get(1, 'a'), null) +assert.equal(a.get(1, 1), null) + +assert.equal(a.put(1), false) +assert.equal(a.put(['a']), false) +assert.equal(a.put('a', 1), false) +assert.equal(a.put(1, 'a'), false) +assert.equal(a.put(1, 1), false) +assert.equal(a.put('a', 'a', 1), false) +assert.equal(a.put('a', 1, 'a'), false) +assert.equal(a.put(1, 'a', 'a'), false) +assert.equal(a.put('a', 1, 1), false) +assert.equal(a.put(1, 'a', 1), false) +assert.equal(a.put(1, 1, 'a'), false) +assert.equal(a.put(1, 1, 1), false) + + +assert.equal(a.get(), null) +assert.equal(a.get('a'), null) +assert.equal(a.get('a', 'c'), null) +assert.equal(a.put(), false) +assert.equal(a.put('a'), false) +assert.equal(a.get('a', 'b', 'c'), null) + +assert(a.put('a', 'axe')) +assert(a.put('a', 'first')) +assert.equal(a.get('a'), 'first') +assert.equal(a.get('a', 'b'), null) +assert.equal(a.get('a', 'c'), null) + +assert(a.put('a', 'apple', 'b')) +assert.equal(a.get('a', 'b'), 'apple') +assert.equal(a.get('a'), 'first') +assert(a.put('b', 'butter', 'b'), 'butter') +assert(a.put('b', 'banana', 'b')) +assert.equal(a.get('b', 'b'), 'banana') +assert.equal(a.get('b'), null) +assert.equal(a.get('b', 'c'), null) + +// Delete +assert.equal(a.delete(1), false) +assert.equal(a.delete('a', 1), false) +assert.equal(a.delete(1, 'a'), false) +assert.equal(a.delete(1, 1), false) + +assert.equal(a.delete('b'), true) +assert(a.delete('a')) +assert.equal(a.get('a'), null) +assert.equal(a.get('a', 'b'), 'apple') +assert.equal(a.delete('c', 'c'), false) +assert.equal(a.delete('c', 'b'), true) +assert(a.delete('b', 'b')) +assert.equal(a.get('b', 'b'), null) + +// Dump +console.log("MARKER 1") +assert(a.dump()) +console.log("Should be no output between 'MARKER 1' and here\n") +console.log('Next line should be "a" => "apple"') +assert(a.dump('b')) + +console.log("\nMARKER 2") +assert.equal(a.dump('c'), false) +console.log("Should be no output between 'MARKER 2' and here\n") + +// WriteBatch + + +// Clean up test database +exec('rm -rf ' + DB_NAME)