Summary: Completed the implementation for the Redis API for Lists. The Redis API uses rocksdb as a backend to persistently store maps from key->list. It supports basic operations for appending, inserting, pushing, popping, and accessing a list, given its key. Test Plan: - Compile with: make redis_test - Test with: ./redis_test - Run all unit tests (for all rocksdb) with: make all check - To use an interactive REDIS client use: ./redis_test -m - To clean the database before use: ./redis_test -m -d Reviewers: haobo, dhruba, zshao Reviewed By: haobo CC: leveldb Differential Revision: https://reviews.facebook.net/D10833main
parent
e673d5d26d
commit
5679107b07
@ -0,0 +1,14 @@ |
||||
This folder defines a REDIS-style interface for Rocksdb. |
||||
Right now it is written as a simple tag-on in the leveldb::RedisLists class. |
||||
It implements Redis Lists, and supports only the "non-blocking operations". |
||||
|
||||
Internally, the set of lists are stored in a rocksdb database, mapping keys to |
||||
values. Each "value" is the list itself, storing a sequence of "elements". |
||||
Each element is stored as a 32-bit-integer, followed by a sequence of bytes. |
||||
The 32-bit-integer represents the length of the element (that is, the number |
||||
of bytes that follow). And then that many bytes follow. |
||||
|
||||
|
||||
NOTE: This README file may be old. See the actual redis_lists.cc file for |
||||
definitive details on the implementation. There should be a header at the top |
||||
of that file, explaining a bit of the implementation details. |
@ -0,0 +1,24 @@ |
||||
/**
|
||||
* A simple structure for exceptions in RedisLists. |
||||
* |
||||
* @author Deon Nicholas (dnicholas@fb.com) |
||||
* Copyright 2013 Facebook |
||||
*/ |
||||
|
||||
#ifndef LEVELDB_REDIS_LIST_EXCEPTION_H |
||||
#define LEVELDB_REDIS_LIST_EXCEPTION_H |
||||
|
||||
#include <exception> |
||||
|
||||
namespace leveldb { |
||||
|
||||
class RedisListException: public std::exception { |
||||
public: |
||||
const char* what() const throw() { |
||||
return "Invalid operation or corrupt data in Redis List."; |
||||
} |
||||
}; |
||||
|
||||
} // namespace leveldb
|
||||
|
||||
#endif // LEVELDB_REDIS_LIST_EXCEPTION_H
|
@ -0,0 +1,306 @@ |
||||
/**
|
||||
* RedisListIterator: |
||||
* An abstraction over the "list" concept (e.g.: for redis lists). |
||||
* Provides functionality to read, traverse, edit, and write these lists. |
||||
* |
||||
* Upon construction, the RedisListIterator is given a block of list data. |
||||
* Internally, it stores a pointer to the data and a pointer to current item. |
||||
* It also stores a "result" list that will be mutated over time. |
||||
* |
||||
* Traversal and mutation are done by "forward iteration". |
||||
* The Push() and Skip() methods will advance the iterator to the next item. |
||||
* However, Push() will also "write the current item to the result". |
||||
* Skip() will simply move to next item, causing current item to be dropped. |
||||
* |
||||
* Upon completion, the result (accessible by WriteResult()) will be saved. |
||||
* All "skipped" items will be gone; all "pushed" items will remain. |
||||
* |
||||
* @throws Any of the operations may throw a RedisListException if an invalid |
||||
* operation is performed or if the data is found to be corrupt. |
||||
* |
||||
* @notes By default, if WriteResult() is called part-way through iteration, |
||||
* it will automatically advance the iterator to the end, and Keep() |
||||
* all items that haven't been traversed yet. This may be subject |
||||
* to review. |
||||
* |
||||
* @notes Can access the "current" item via GetCurrent(), and other |
||||
* list-specific information such as Length(). |
||||
* |
||||
* @notes The internal representation is due to change at any time. Presently, |
||||
* the list is represented as follows: |
||||
* - 32-bit integer header: the number of items in the list |
||||
* - For each item: |
||||
* - 32-bit int (n): the number of bytes representing this item |
||||
* - n bytes of data: the actual data. |
||||
* |
||||
* @author Deon Nicholas (dnicholas@fb.com) |
||||
* Copyright 2013 Facebook |
||||
*/ |
||||
|
||||
#include <string> |
||||
|
||||
#include "redis_list_exception.h" |
||||
#include "leveldb/slice.h" |
||||
#include "util/coding.h" |
||||
|
||||
namespace leveldb { |
||||
|
||||
/// An abstraction over the "list" concept.
|
||||
/// All operations may throw a RedisListException
|
||||
class RedisListIterator { |
||||
public: |
||||
/// Construct a redis-list-iterator based on data.
|
||||
/// If the data is non-empty, it must formatted according to @notes above.
|
||||
///
|
||||
/// If the data is valid, we can assume the following invariant(s):
|
||||
/// a) length_, num_bytes_ are set correctly.
|
||||
/// b) cur_byte_ always refers to the start of the current element,
|
||||
/// just before the bytes that specify element length.
|
||||
/// c) cur_elem_ is always the index of the current element.
|
||||
/// d) cur_elem_length_ is always the number of bytes in current element,
|
||||
/// excluding the 4-byte header itself.
|
||||
/// e) result_ will always contain data_[0..cur_byte_) and a header
|
||||
/// f) Whenever corrupt data is encountered or an invalid operation is
|
||||
/// attempted, a RedisListException will immediately be thrown.
|
||||
RedisListIterator(const std::string& list_data) |
||||
: data_(list_data.data()), |
||||
num_bytes_(list_data.size()), |
||||
cur_byte_(0), |
||||
cur_elem_(0), |
||||
cur_elem_length_(0), |
||||
length_(0), |
||||
result_() { |
||||
|
||||
// Initialize the result_ (reserve enough space for header)
|
||||
InitializeResult(); |
||||
|
||||
// Parse the data only if it is not empty.
|
||||
if (num_bytes_ == 0) { |
||||
return; |
||||
} |
||||
|
||||
// If non-empty, but less than 4 bytes, data must be corrupt
|
||||
if (num_bytes_ < sizeof(length_)) { |
||||
ThrowError("Corrupt header."); // Will break control flow
|
||||
} |
||||
|
||||
// Good. The first bytes specify the number of elements
|
||||
length_ = DecodeFixed32(data_); |
||||
cur_byte_ = sizeof(length_); |
||||
|
||||
// If we have at least one element, point to that element.
|
||||
// Also, read the first integer of the element (specifying the size),
|
||||
// if possible.
|
||||
if (length_ > 0) { |
||||
if (cur_byte_ + sizeof(cur_elem_length_) <= num_bytes_) { |
||||
cur_elem_length_ = DecodeFixed32(data_+cur_byte_); |
||||
} else { |
||||
ThrowError("Corrupt data for first element."); |
||||
} |
||||
} |
||||
|
||||
// At this point, we are fully set-up.
|
||||
// The invariants described in the header should now be true.
|
||||
} |
||||
|
||||
/// Reserve some space for the result_.
|
||||
/// Equivalent to result_.reserve(bytes).
|
||||
void Reserve(int bytes) { |
||||
result_.reserve(bytes); |
||||
} |
||||
|
||||
/// Go to next element in data file.
|
||||
/// Also writes the current element to result_.
|
||||
RedisListIterator& Push() { |
||||
WriteCurrentElement(); |
||||
MoveNext(); |
||||
return *this; |
||||
} |
||||
|
||||
/// Go to next element in data file.
|
||||
/// Drops/skips the current element. It will not be written to result_.
|
||||
RedisListIterator& Skip() { |
||||
MoveNext(); |
||||
--length_; // One less item
|
||||
--cur_elem_; // We moved one forward, but index did not change
|
||||
return *this; |
||||
} |
||||
|
||||
/// Insert elem into the result_ (just BEFORE the current element / byte)
|
||||
/// Note: if Done() (i.e.: iterator points to end), this will append elem.
|
||||
void InsertElement(const Slice& elem) { |
||||
// Ensure we are in a valid state
|
||||
CheckErrors(); |
||||
|
||||
const int kOrigSize = result_.size(); |
||||
result_.resize(kOrigSize + SizeOf(elem)); |
||||
EncodeFixed32(result_.data() + kOrigSize, elem.size()); |
||||
memcpy(result_.data() + kOrigSize + sizeof(uint32_t), |
||||
elem.data(), |
||||
elem.size()); |
||||
++length_; |
||||
++cur_elem_; |
||||
} |
||||
|
||||
/// Access the current element, and save the result into *curElem
|
||||
void GetCurrent(Slice* curElem) { |
||||
// Ensure we are in a valid state
|
||||
CheckErrors(); |
||||
|
||||
// Ensure that we are not past the last element.
|
||||
if (Done()) { |
||||
ThrowError("Invalid dereferencing."); |
||||
} |
||||
|
||||
// Dereference the element
|
||||
*curElem = Slice(data_+cur_byte_+sizeof(cur_elem_length_), |
||||
cur_elem_length_); |
||||
} |
||||
|
||||
// Number of elements
|
||||
int Length() const { |
||||
return length_; |
||||
} |
||||
|
||||
// Number of bytes in the final representation (i.e: WriteResult().size())
|
||||
int Size() const { |
||||
// result_ holds the currently written data
|
||||
// data_[cur_byte..num_bytes-1] is the remainder of the data
|
||||
return result_.size() + (num_bytes_ - cur_byte_); |
||||
} |
||||
|
||||
// Reached the end?
|
||||
bool Done() const { |
||||
return cur_byte_ >= num_bytes_ || cur_elem_ >= length_; |
||||
} |
||||
|
||||
/// Returns a string representing the final, edited, data.
|
||||
/// Assumes that all bytes of data_ in the range [0,cur_byte_) have been read
|
||||
/// and that result_ contains this data.
|
||||
/// The rest of the data must still be written.
|
||||
/// So, this method ADVANCES THE ITERATOR TO THE END before writing.
|
||||
Slice WriteResult() { |
||||
CheckErrors(); |
||||
|
||||
// The header should currently be filled with dummy data (0's)
|
||||
// Correctly update the header.
|
||||
// Note, this is safe since result_ is a vector (guaranteed contiguous)
|
||||
EncodeFixed32(&result_[0],length_); |
||||
|
||||
// Append the remainder of the data to the result.
|
||||
result_.insert(result_.end(),data_+cur_byte_, data_ +num_bytes_); |
||||
|
||||
// Seek to end of file
|
||||
cur_byte_ = num_bytes_; |
||||
cur_elem_ = length_; |
||||
cur_elem_length_ = 0; |
||||
|
||||
// Return the result
|
||||
return Slice(result_.data(),result_.size()); |
||||
} |
||||
|
||||
public: // Static public functions
|
||||
|
||||
/// An upper-bound on the amount of bytes needed to store this element.
|
||||
/// This is used to hide representation information from the client.
|
||||
/// E.G. This can be used to compute the bytes we want to Reserve().
|
||||
static uint32_t SizeOf(const Slice& elem) { |
||||
// [Integer Length . Data]
|
||||
return sizeof(uint32_t) + elem.size(); |
||||
} |
||||
|
||||
private: // Private functions
|
||||
|
||||
/// Initializes the result_ string.
|
||||
/// It will fill the first few bytes with 0's so that there is
|
||||
/// enough space for header information when we need to write later.
|
||||
/// Currently, "header information" means: the length (number of elements)
|
||||
/// Assumes that result_ is empty to begin with
|
||||
void InitializeResult() { |
||||
assert(result_.empty()); // Should always be true.
|
||||
result_.resize(sizeof(uint32_t),0); // Put a block of 0's as the header
|
||||
} |
||||
|
||||
/// Go to the next element (used in Push() and Skip())
|
||||
void MoveNext() { |
||||
CheckErrors(); |
||||
|
||||
// Check to make sure we are not already in a finished state
|
||||
if (Done()) { |
||||
ThrowError("Attempting to iterate past end of list."); |
||||
} |
||||
|
||||
// Move forward one element.
|
||||
cur_byte_ += sizeof(cur_elem_length_) + cur_elem_length_; |
||||
++cur_elem_; |
||||
|
||||
// If we are at the end, finish
|
||||
if (Done()) { |
||||
cur_elem_length_ = 0; |
||||
return; |
||||
} |
||||
|
||||
// Otherwise, we should be able to read the new element's length
|
||||
if (cur_byte_ + sizeof(cur_elem_length_) > num_bytes_) { |
||||
ThrowError("Corrupt element data."); |
||||
} |
||||
|
||||
// Set the new element's length
|
||||
cur_elem_length_ = DecodeFixed32(data_+cur_byte_); |
||||
|
||||
return; |
||||
} |
||||
|
||||
/// Append the current element (pointed to by cur_byte_) to result_
|
||||
/// Assumes result_ has already been reserved appropriately.
|
||||
void WriteCurrentElement() { |
||||
// First verify that the iterator is still valid.
|
||||
CheckErrors(); |
||||
if (Done()) { |
||||
ThrowError("Attempting to write invalid element."); |
||||
} |
||||
|
||||
// Append the cur element.
|
||||
result_.insert(result_.end(), |
||||
data_+cur_byte_, |
||||
data_+cur_byte_+ sizeof(uint32_t) + cur_elem_length_); |
||||
} |
||||
|
||||
/// Will ThrowError() if neccessary.
|
||||
/// Checks for common/ubiquitous errors that can arise after most operations.
|
||||
/// This method should be called before any reading operation.
|
||||
/// If this function succeeds, then we are guaranteed to be in a valid state.
|
||||
/// Other member functions should check for errors and ThrowError() also
|
||||
/// if an error occurs that is specific to it even while in a valid state.
|
||||
void CheckErrors() { |
||||
// Check if any crazy thing has happened recently
|
||||
if ((cur_elem_ > length_) || // Bad index
|
||||
(cur_byte_ > num_bytes_) || // No more bytes
|
||||
(cur_byte_ + cur_elem_length_ > num_bytes_) || // Item too large
|
||||
(cur_byte_ == num_bytes_ && cur_elem_ != length_) || // Too many items
|
||||
(cur_elem_ == length_ && cur_byte_ != num_bytes_)) { // Too many bytes
|
||||
ThrowError("Corrupt data."); |
||||
} |
||||
} |
||||
|
||||
/// Will throw an exception based on the passed-in message.
|
||||
/// This function is guaranteed to STOP THE CONTROL-FLOW.
|
||||
/// (i.e.: you do not have to call "return" after calling ThrowError)
|
||||
void ThrowError(const char* const msg = NULL) { |
||||
// TODO: For now we ignore the msg parameter. This can be expanded later.
|
||||
throw RedisListException(); |
||||
} |
||||
|
||||
private: |
||||
const char* const data_; // A pointer to the data (the first byte)
|
||||
const uint32_t num_bytes_; // The number of bytes in this list
|
||||
|
||||
uint32_t cur_byte_; // The current byte being read
|
||||
uint32_t cur_elem_; // The current element being read
|
||||
uint32_t cur_elem_length_; // The number of bytes in current element
|
||||
|
||||
uint32_t length_; // The number of elements in this list
|
||||
std::vector<char> result_; // The output data
|
||||
}; |
||||
|
||||
} // namespace leveldb
|
@ -0,0 +1,551 @@ |
||||
/**
|
||||
* A (persistent) Redis API built using the rocksdb backend. |
||||
* Implements Redis Lists as described on: http://redis.io/commands#list
|
||||
* |
||||
* @throws All functions may throw a RedisListException on error/corruption. |
||||
* |
||||
* @notes Internally, the set of lists is stored in a rocksdb database, |
||||
* mapping keys to values. Each "value" is the list itself, storing |
||||
* some kind of internal representation of the data. All the |
||||
* representation details are handled by the RedisListIterator class. |
||||
* The present file should be oblivious to the representation details, |
||||
* handling only the client (Redis) API, and the calls to rocksdb. |
||||
* |
||||
* @TODO Presently, all operations take at least O(NV) time where |
||||
* N is the number of elements in the list, and V is the average |
||||
* number of bytes per value in the list. So maybe, with merge operator |
||||
* we can improve this to an optimal O(V) amortized time, since we |
||||
* wouldn't have to read and re-write the entire list. |
||||
* |
||||
* @author Deon Nicholas (dnicholas@fb.com) |
||||
* Copyright 2013 Facebook |
||||
*/ |
||||
|
||||
#include "redis_lists.h" |
||||
|
||||
#include <iostream> |
||||
#include <memory> |
||||
#include <cmath> |
||||
|
||||
#include "leveldb/slice.h" |
||||
#include "util/coding.h" |
||||
|
||||
namespace leveldb |
||||
{ |
||||
|
||||
/// Constructors
|
||||
|
||||
RedisLists::RedisLists(const std::string& db_path, |
||||
Options options, bool destructive) |
||||
: put_option_(), |
||||
get_option_() { |
||||
|
||||
// Store the name of the database
|
||||
db_name_ = db_path; |
||||
|
||||
// If destructive, destroy the DB before re-opening it.
|
||||
if (destructive) { |
||||
DestroyDB(db_name_, Options()); |
||||
} |
||||
|
||||
// Now open and deal with the db
|
||||
DB* db; |
||||
Status s = DB::Open(options, db_name_, &db); |
||||
if (!s.ok()) { |
||||
std::cerr << "ERROR " << s.ToString() << std::endl; |
||||
assert(false); |
||||
} |
||||
|
||||
db_ = std::unique_ptr<DB>(db); |
||||
} |
||||
|
||||
|
||||
/// Accessors
|
||||
|
||||
// Number of elements in the list associated with key
|
||||
// : throws RedisListException
|
||||
int RedisLists::Length(const std::string& key) { |
||||
// Extract the string data representing the list.
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Return the length
|
||||
RedisListIterator it(data); |
||||
return it.Length(); |
||||
} |
||||
|
||||
// Get the element at the specified index in the (list: key)
|
||||
// Returns <empty> ("") on out-of-bounds
|
||||
// : throws RedisListException
|
||||
bool RedisLists::Index(const std::string& key, int32_t index, |
||||
std::string* result) { |
||||
// Extract the string data representing the list.
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Handle REDIS negative indices (from the end); fast iff Length() takes O(1)
|
||||
if (index < 0) { |
||||
index = Length(key) - (-index); //replace (-i) with (N-i).
|
||||
} |
||||
|
||||
// Iterate through the list until the desired index is found.
|
||||
int curIndex = 0; |
||||
RedisListIterator it(data); |
||||
while(curIndex < index && !it.Done()) { |
||||
++curIndex; |
||||
it.Skip(); |
||||
} |
||||
|
||||
// If we actually found the index
|
||||
if (curIndex == index && !it.Done()) { |
||||
Slice elem; |
||||
it.GetCurrent(&elem); |
||||
if (result != NULL) { |
||||
*result = elem.ToString(); |
||||
} |
||||
|
||||
return true; |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
// Return a truncated version of the list.
|
||||
// First, negative values for first/last are interpreted as "end of list".
|
||||
// So, if first == -1, then it is re-set to index: (Length(key) - 1)
|
||||
// Then, return exactly those indices i such that first <= i <= last.
|
||||
// : throws RedisListException
|
||||
std::vector<std::string> RedisLists::Range(const std::string& key, |
||||
int32_t first, int32_t last) { |
||||
// Extract the string data representing the list.
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Handle negative bounds (-1 means last element, etc.)
|
||||
int listLen = Length(key); |
||||
if (first < 0) { |
||||
first = listLen - (-first); // Replace (-x) with (N-x)
|
||||
} |
||||
if (last < 0) { |
||||
last = listLen - (-last); |
||||
} |
||||
|
||||
// Verify bounds (and truncate the range so that it is valid)
|
||||
first = std::max(first, 0); |
||||
last = std::min(last, listLen-1); |
||||
int len = std::max(last-first+1, 0); |
||||
|
||||
// Initialize the resulting list
|
||||
std::vector<std::string> result(len); |
||||
|
||||
// Traverse the list and update the vector
|
||||
int curIdx = 0; |
||||
Slice elem; |
||||
for (RedisListIterator it(data); !it.Done() && curIdx<=last; it.Skip()) { |
||||
if (first <= curIdx && curIdx <= last) { |
||||
it.GetCurrent(&elem); |
||||
result[curIdx-first].assign(elem.data(),elem.size()); |
||||
} |
||||
|
||||
++curIdx; |
||||
} |
||||
|
||||
// Return the result. Might be empty
|
||||
return result; |
||||
} |
||||
|
||||
// Print the (list: key) out to stdout. For debugging mostly. Public for now.
|
||||
void RedisLists::Print(const std::string& key) { |
||||
// Extract the string data representing the list.
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Iterate through the list and print the items
|
||||
Slice elem; |
||||
for (RedisListIterator it(data); !it.Done(); it.Skip()) { |
||||
it.GetCurrent(&elem); |
||||
std::cout << "ITEM " << elem.ToString() << std::endl; |
||||
} |
||||
|
||||
//Now print the byte data
|
||||
RedisListIterator it(data); |
||||
std::cout << "==Printing data==" << std::endl; |
||||
std::cout << data.size() << std::endl; |
||||
std::cout << it.Size() << " " << it.Length() << std::endl; |
||||
Slice result = it.WriteResult(); |
||||
std::cout << result.data() << std::endl; |
||||
if (true) { |
||||
std::cout << "size: " << result.size() << std::endl; |
||||
const char* val = result.data(); |
||||
for(int i=0; i<(int)result.size(); ++i) { |
||||
std::cout << (int)val[i] << " " << (val[i]>=32?val[i]:' ') << std::endl; |
||||
} |
||||
std::cout << std::endl; |
||||
} |
||||
} |
||||
|
||||
/// Insert/Update Functions
|
||||
/// Note: The "real" insert function is private. See below.
|
||||
|
||||
// InsertBefore and InsertAfter are simply wrappers around the Insert function.
|
||||
int RedisLists::InsertBefore(const std::string& key, const std::string& pivot, |
||||
const std::string& value) { |
||||
return Insert(key, pivot, value, false); |
||||
} |
||||
|
||||
int RedisLists::InsertAfter(const std::string& key, const std::string& pivot, |
||||
const std::string& value) { |
||||
return Insert(key, pivot, value, true); |
||||
} |
||||
|
||||
// Prepend value onto beginning of (list: key)
|
||||
// : throws RedisListException
|
||||
int RedisLists::PushLeft(const std::string& key, const std::string& value) { |
||||
// Get the original list data
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Construct the result
|
||||
RedisListIterator it(data); |
||||
it.Reserve(it.Size() + it.SizeOf(value)); |
||||
it.InsertElement(value); |
||||
|
||||
// Push the data back to the db and return the length
|
||||
db_->Put(put_option_, key, it.WriteResult()); |
||||
return it.Length(); |
||||
} |
||||
|
||||
// Append value onto end of (list: key)
|
||||
// TODO: Make this O(1) time. Might require MergeOperator.
|
||||
// : throws RedisListException
|
||||
int RedisLists::PushRight(const std::string& key, const std::string& value) { |
||||
// Get the original list data
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Create an iterator to the data and seek to the end.
|
||||
RedisListIterator it(data); |
||||
it.Reserve(it.Size() + it.SizeOf(value)); |
||||
while (!it.Done()) { |
||||
it.Push(); // Write each element as we go
|
||||
} |
||||
|
||||
// Insert the new element at the current position (the end)
|
||||
it.InsertElement(value); |
||||
|
||||
// Push it back to the db, and return length
|
||||
db_->Put(put_option_, key, it.WriteResult()); |
||||
return it.Length(); |
||||
} |
||||
|
||||
// Set (list: key)[idx] = val. Return true on success, false on fail.
|
||||
// : throws RedisListException
|
||||
bool RedisLists::Set(const std::string& key, int32_t index, |
||||
const std::string& value) { |
||||
// Get the original list data
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Handle negative index for REDIS (meaning -index from end of list)
|
||||
if (index < 0) { |
||||
index = Length(key) - (-index); |
||||
} |
||||
|
||||
// Iterate through the list until we find the element we want
|
||||
int curIndex = 0; |
||||
RedisListIterator it(data); |
||||
it.Reserve(it.Size() + it.SizeOf(value)); // Over-estimate is fine
|
||||
while(curIndex < index && !it.Done()) { |
||||
it.Push(); |
||||
++curIndex; |
||||
} |
||||
|
||||
// If not found, return false (this occurs when index was invalid)
|
||||
if (it.Done() || curIndex != index) { |
||||
return false; |
||||
} |
||||
|
||||
// Write the new element value, and drop the previous element value
|
||||
it.InsertElement(value); |
||||
it.Skip(); |
||||
|
||||
// Write the data to the database
|
||||
// Check status, since it needs to return true/false guarantee
|
||||
Status s = db_->Put(put_option_, key, it.WriteResult()); |
||||
|
||||
// Success
|
||||
return s.ok(); |
||||
} |
||||
|
||||
/// Delete / Remove / Pop functions
|
||||
|
||||
// Trim (list: key) so that it will only contain the indices from start..stop
|
||||
// Invalid indices will not generate an error, just empty,
|
||||
// or the portion of the list that fits in this interval
|
||||
// : throws RedisListException
|
||||
bool RedisLists::Trim(const std::string& key, int32_t start, int32_t stop) { |
||||
// Get the original list data
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Handle negative indices in REDIS
|
||||
int listLen = Length(key); |
||||
if (start < 0) { |
||||
start = listLen - (-start); |
||||
} |
||||
if (stop < 0) { |
||||
stop = listLen - (-stop); |
||||
} |
||||
|
||||
// Truncate bounds to only fit in the list
|
||||
start = std::max(start, 0); |
||||
stop = std::min(stop, listLen-1); |
||||
|
||||
// Construct an iterator for the list. Drop all undesired elements.
|
||||
int curIndex = 0; |
||||
RedisListIterator it(data); |
||||
it.Reserve(it.Size()); // Over-estimate
|
||||
while(!it.Done()) { |
||||
// If not within the range, just skip the item (drop it).
|
||||
// Otherwise, continue as usual.
|
||||
if (start <= curIndex && curIndex <= stop) { |
||||
it.Push(); |
||||
} else { |
||||
it.Skip(); |
||||
} |
||||
|
||||
// Increment the current index
|
||||
++curIndex; |
||||
} |
||||
|
||||
// Write the (possibly empty) result to the database
|
||||
Status s = db_->Put(put_option_, key, it.WriteResult()); |
||||
|
||||
// Return true as long as the write succeeded
|
||||
return s.ok(); |
||||
} |
||||
|
||||
// Return and remove the first element in the list (or "" if empty)
|
||||
// : throws RedisListException
|
||||
bool RedisLists::PopLeft(const std::string& key, std::string* result) { |
||||
// Get the original list data
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Point to first element in the list (if it exists), and get its value/size
|
||||
RedisListIterator it(data); |
||||
if (it.Length() > 0) { // Proceed only if list is non-empty
|
||||
Slice elem; |
||||
it.GetCurrent(&elem); // Store the value of the first element
|
||||
it.Reserve(it.Size() - it.SizeOf(elem)); |
||||
it.Skip(); // DROP the first item and move to next
|
||||
|
||||
// Update the db
|
||||
db_->Put(put_option_, key, it.WriteResult()); |
||||
|
||||
// Return the value
|
||||
if (result != NULL) { |
||||
*result = elem.ToString(); |
||||
} |
||||
return true; |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
// Remove and return the last element in the list (or "" if empty)
|
||||
// TODO: Make this O(1). Might require MergeOperator.
|
||||
// : throws RedisListException
|
||||
bool RedisLists::PopRight(const std::string& key, std::string* result) { |
||||
// Extract the original list data
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Construct an iterator to the data and move to last element
|
||||
RedisListIterator it(data); |
||||
it.Reserve(it.Size()); |
||||
int len = it.Length(); |
||||
int curIndex = 0; |
||||
while(curIndex < (len-1) && !it.Done()) { |
||||
it.Push(); |
||||
++curIndex; |
||||
} |
||||
|
||||
// Extract and drop/skip the last element
|
||||
if (curIndex == len-1) { |
||||
assert(!it.Done()); // Sanity check. Should not have ended here.
|
||||
|
||||
// Extract and pop the element
|
||||
Slice elem; |
||||
it.GetCurrent(&elem); // Save value of element.
|
||||
it.Skip(); // Skip the element
|
||||
|
||||
// Write the result to the database
|
||||
db_->Put(put_option_, key, it.WriteResult()); |
||||
|
||||
// Return the value
|
||||
if (result != NULL) { |
||||
*result = elem.ToString(); |
||||
} |
||||
return true; |
||||
} else { |
||||
// Must have been an empty list
|
||||
assert(it.Done() && len==0 && curIndex == 0); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
// Remove the (first or last) "num" occurrences of value in (list: key)
|
||||
// : throws RedisListException
|
||||
int RedisLists::Remove(const std::string& key, int32_t num, |
||||
const std::string& value) { |
||||
// Negative num ==> RemoveLast; Positive num ==> Remove First
|
||||
if (num < 0) { |
||||
return RemoveLast(key, -num, value); |
||||
} else if (num > 0) { |
||||
return RemoveFirst(key, num, value); |
||||
} else { |
||||
return RemoveFirst(key, Length(key), value); |
||||
} |
||||
} |
||||
|
||||
// Remove the first "num" occurrences of value in (list: key).
|
||||
// : throws RedisListException
|
||||
int RedisLists::RemoveFirst(const std::string& key, int32_t num, |
||||
const std::string& value) { |
||||
// Ensure that the number is positive
|
||||
assert(num >= 0); |
||||
|
||||
// Extract the original list data
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Traverse the list, appending all but the desired occurrences of value
|
||||
int numSkipped = 0; // Keep track of the number of times value is seen
|
||||
Slice elem; |
||||
RedisListIterator it(data); |
||||
it.Reserve(it.Size()); |
||||
while (!it.Done()) { |
||||
it.GetCurrent(&elem); |
||||
|
||||
if (elem == value && numSkipped < num) { |
||||
// Drop this item if desired
|
||||
it.Skip(); |
||||
++numSkipped; |
||||
} else { |
||||
// Otherwise keep the item and proceed as normal
|
||||
it.Push(); |
||||
} |
||||
} |
||||
|
||||
// Put the result back to the database
|
||||
db_->Put(put_option_, key, it.WriteResult()); |
||||
|
||||
// Return the number of elements removed
|
||||
return numSkipped; |
||||
} |
||||
|
||||
|
||||
// Remove the last "num" occurrences of value in (list: key).
|
||||
// TODO: I traverse the list 2x. Make faster. Might require MergeOperator.
|
||||
// : throws RedisListException
|
||||
int RedisLists::RemoveLast(const std::string& key, int32_t num, |
||||
const std::string& value) { |
||||
// Ensure that the number is positive
|
||||
assert(num >= 0); |
||||
|
||||
// Extract the original list data
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Temporary variable to hold the "current element" in the blocks below
|
||||
Slice elem; |
||||
|
||||
// Count the total number of occurrences of value
|
||||
int totalOccs = 0; |
||||
for (RedisListIterator it(data); !it.Done(); it.Skip()) { |
||||
it.GetCurrent(&elem); |
||||
if (elem == value) { |
||||
++totalOccs; |
||||
} |
||||
} |
||||
|
||||
// Construct an iterator to the data. Reserve enough space for the result.
|
||||
RedisListIterator it(data); |
||||
int bytesRemoved = std::min(num,totalOccs)*it.SizeOf(value); |
||||
it.Reserve(it.Size() - bytesRemoved); |
||||
|
||||
// Traverse the list, appending all but the desired occurrences of value.
|
||||
// Note: "Drop the last k occurrences" is equivalent to
|
||||
// "keep only the first n-k occurrences", where n is total occurrences.
|
||||
int numKept = 0; // Keep track of the number of times value is kept
|
||||
while(!it.Done()) { |
||||
it.GetCurrent(&elem); |
||||
|
||||
// If we are within the deletion range and equal to value, drop it.
|
||||
// Otherwise, append/keep/push it.
|
||||
if (elem == value) { |
||||
if (numKept < totalOccs - num) { |
||||
it.Push(); |
||||
++numKept; |
||||
} else { |
||||
it.Skip(); |
||||
} |
||||
} else { |
||||
// Always append the others
|
||||
it.Push(); |
||||
} |
||||
} |
||||
|
||||
// Put the result back to the database
|
||||
db_->Put(put_option_, key, it.WriteResult()); |
||||
|
||||
// Return the number of elements removed
|
||||
return totalOccs - numKept; |
||||
} |
||||
|
||||
/// Private functions
|
||||
|
||||
// Insert element value into (list: key), right before/after
|
||||
// the first occurrence of pivot
|
||||
// : throws RedisListException
|
||||
int RedisLists::Insert(const std::string& key, const std::string& pivot, |
||||
const std::string& value, bool insert_after) { |
||||
// Get the original list data
|
||||
std::string data; |
||||
db_->Get(get_option_, key, &data); |
||||
|
||||
// Construct an iterator to the data and reserve enough space for result.
|
||||
RedisListIterator it(data); |
||||
it.Reserve(it.Size() + it.SizeOf(value)); |
||||
|
||||
// Iterate through the list until we find the element we want
|
||||
Slice elem; |
||||
bool found = false; |
||||
while(!it.Done() && !found) { |
||||
it.GetCurrent(&elem); |
||||
|
||||
// When we find the element, insert the element and mark found
|
||||
if (elem == pivot) { // Found it!
|
||||
found = true; |
||||
if (insert_after == true) { // Skip one more, if inserting after it
|
||||
it.Push(); |
||||
} |
||||
it.InsertElement(value); |
||||
} else { |
||||
it.Push(); |
||||
} |
||||
|
||||
} |
||||
|
||||
// Put the data (string) into the database
|
||||
if (found) { |
||||
db_->Put(put_option_, key, it.WriteResult()); |
||||
} |
||||
|
||||
// Returns the new (possibly unchanged) length of the list
|
||||
return it.Length(); |
||||
} |
||||
|
||||
|
||||
} |
@ -0,0 +1,104 @@ |
||||
/**
|
||||
* A (persistent) Redis API built using the rocksdb backend. |
||||
* Implements Redis Lists as described on: http://redis.io/commands#list
|
||||
* |
||||
* @throws All functions may throw a RedisListException |
||||
* |
||||
* @author Deon Nicholas (dnicholas@fb.com) |
||||
* Copyright 2013 Facebook |
||||
*/ |
||||
|
||||
#include <string> |
||||
#include "leveldb/db.h" |
||||
#include "redis_list_iterator.h" |
||||
#include "redis_list_exception.h" |
||||
|
||||
namespace leveldb { |
||||
|
||||
/// The Redis functionality (see http://redis.io/commands#list)
|
||||
/// All functions may THROW a RedisListException
|
||||
class RedisLists { |
||||
public: // Constructors / Destructors
|
||||
/// Construct a new RedisLists database, with name/path of db.
|
||||
/// Will clear the database on open iff destructive is true (default false).
|
||||
/// Otherwise, it will restore saved changes.
|
||||
/// May throw RedisListException
|
||||
RedisLists(const std::string& db_path, |
||||
Options options, bool destructive = false); |
||||
|
||||
public: // Accessors
|
||||
/// The number of items in (list: key)
|
||||
int Length(const std::string& key); |
||||
|
||||
/// Search the list for the (index)'th item (0-based) in (list:key)
|
||||
/// A negative index indicates: "from end-of-list"
|
||||
/// If index is within range: return true, and return the value in *result.
|
||||
/// If (index < -length OR index>=length), then index is out of range:
|
||||
/// return false (and *result is left unchanged)
|
||||
/// May throw RedisListException
|
||||
bool Index(const std::string& key, int32_t index, |
||||
std::string* result); |
||||
|
||||
/// Return (list: key)[first..last] (inclusive)
|
||||
/// May throw RedisListException
|
||||
std::vector<std::string> Range(const std::string& key, |
||||
int32_t first, int32_t last); |
||||
|
||||
/// Prints the entire (list: key), for debugging.
|
||||
void Print(const std::string& key); |
||||
|
||||
public: // Insert/Update
|
||||
/// Insert value before/after pivot in (list: key). Return the length.
|
||||
/// May throw RedisListException
|
||||
int InsertBefore(const std::string& key, const std::string& pivot, |
||||
const std::string& value); |
||||
int InsertAfter(const std::string& key, const std::string& pivot, |
||||
const std::string& value); |
||||
|
||||
/// Push / Insert value at beginning/end of the list. Return the length.
|
||||
/// May throw RedisListException
|
||||
int PushLeft(const std::string& key, const std::string& value); |
||||
int PushRight(const std::string& key, const std::string& value); |
||||
|
||||
/// Set (list: key)[idx] = val. Return true on success, false on fail
|
||||
/// May throw RedisListException
|
||||
bool Set(const std::string& key, int32_t index, const std::string& value); |
||||
|
||||
public: // Delete / Remove / Pop / Trim
|
||||
/// Trim (list: key) so that it will only contain the indices from start..stop
|
||||
/// Returns true on success
|
||||
/// May throw RedisListException
|
||||
bool Trim(const std::string& key, int32_t start, int32_t stop); |
||||
|
||||
/// If list is empty, return false and leave *result unchanged.
|
||||
/// Else, remove the first/last elem, store it in *result, and return true
|
||||
bool PopLeft(const std::string& key, std::string* result); // First
|
||||
bool PopRight(const std::string& key, std::string* result); // Last
|
||||
|
||||
/// Remove the first (or last) num occurrences of value from the list (key)
|
||||
/// Return the number of elements removed.
|
||||
/// May throw RedisListException
|
||||
int Remove(const std::string& key, int32_t num, |
||||
const std::string& value); |
||||
int RemoveFirst(const std::string& key, int32_t num, |
||||
const std::string& value); |
||||
int RemoveLast(const std::string& key, int32_t num, |
||||
const std::string& value); |
||||
|
||||
private: // Private Functions
|
||||
/// Calls InsertBefore or InsertAfter
|
||||
int Insert(const std::string& key, const std::string& pivot, |
||||
const std::string& value, bool insert_after); |
||||
private: |
||||
std::string db_name_; // The actual database name/path
|
||||
WriteOptions put_option_; |
||||
ReadOptions get_option_; |
||||
|
||||
/// The backend rocksdb database.
|
||||
/// Map : key --> list
|
||||
/// where a list is a sequence of elements
|
||||
/// and an element is a 4-byte integer (n), followed by n bytes of data
|
||||
std::unique_ptr<DB> db_; |
||||
}; |
||||
|
||||
} // namespace leveldb
|
@ -0,0 +1,900 @@ |
||||
/**
|
||||
* A test harness for the Redis API built on rocksdb. |
||||
* |
||||
* USAGE: Build with: "make redis_test" (in rocksdb directory). |
||||
* Run unit tests with: "./redis_test" |
||||
* Manual/Interactive user testing: "./redis_test -m" |
||||
* Manual user testing + restart database: "./redis_test -m -d" |
||||
* |
||||
* TODO: Add LARGE random test cases to verify efficiency and scalability |
||||
* |
||||
* @author Deon Nicholas (dnicholas@fb.com) |
||||
* Copyright 2013 Facebook |
||||
*/ |
||||
|
||||
|
||||
#include <iostream> |
||||
#include <cctype> |
||||
|
||||
#include "redis_lists.h" |
||||
#include "util/testharness.h" |
||||
#include "util/random.h" |
||||
|
||||
using namespace leveldb; |
||||
using namespace std; |
||||
|
||||
namespace leveldb { |
||||
|
||||
class RedisListsTest { |
||||
public: |
||||
static const string kDefaultDbName; |
||||
static Options options; |
||||
|
||||
RedisListsTest() { |
||||
options.create_if_missing = true; |
||||
} |
||||
}; |
||||
|
||||
const string RedisListsTest::kDefaultDbName = "/tmp/redisdefaultdb/"; |
||||
Options RedisListsTest::options = Options(); |
||||
|
||||
// operator== and operator<< are defined below for vectors (lists)
|
||||
// Needed for ASSERT_EQ
|
||||
|
||||
// Compare two lists for equality.
|
||||
bool operator==(const std::vector<std::string>& a, |
||||
const std::vector<std::string>& b) { |
||||
if (a.size() != b.size()) { |
||||
return false; |
||||
} |
||||
|
||||
int n = a.size(); |
||||
for (int i=0; i<n; ++i) { |
||||
if (a[i] != b[i]) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
// Print out a list
|
||||
ostream& operator<<(ostream& out, const std::vector<std::string>& vec) { |
||||
out << "["; |
||||
int n = vec.size(); |
||||
for(int i=0; i<n; ++i) { |
||||
if (i > 0) { |
||||
out << ", "; |
||||
} |
||||
out << vec[i]; |
||||
} |
||||
out << "]"; |
||||
return out; |
||||
} |
||||
|
||||
/// THE TEST CASES BEGIN HERE
|
||||
|
||||
// PushRight, Length, Index, Range
|
||||
TEST(RedisListsTest, SimpleTest) { |
||||
RedisLists redis(kDefaultDbName, options, true); // Destructive
|
||||
|
||||
string tempv; // Used below for all Index(), PopRight(), PopLeft()
|
||||
|
||||
// Simple PushRight (should return the new length each time)
|
||||
ASSERT_EQ(redis.PushRight("k1", "v1"), 1); |
||||
ASSERT_EQ(redis.PushRight("k1", "v2"), 2); |
||||
ASSERT_EQ(redis.PushRight("k1", "v3"), 3); |
||||
|
||||
// Check Length and Index() functions
|
||||
ASSERT_EQ(redis.Length("k1"), 3); // Check length
|
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "v1"); // Check valid indices
|
||||
ASSERT_TRUE(redis.Index("k1", 1, &tempv)); |
||||
ASSERT_EQ(tempv, "v2"); |
||||
ASSERT_TRUE(redis.Index("k1", 2, &tempv)); |
||||
ASSERT_EQ(tempv, "v3"); |
||||
|
||||
// Check range function and vectors
|
||||
std::vector<std::string> result = redis.Range("k1", 0, 2); // Get the list
|
||||
std::vector<std::string> expected_result(3); |
||||
expected_result[0] = "v1"; |
||||
expected_result[1] = "v2"; |
||||
expected_result[2] = "v3"; |
||||
ASSERT_EQ(result, expected_result); // Uses my overloaded operator==() above
|
||||
} |
||||
|
||||
// PushLeft, Length, Index, Range
|
||||
TEST(RedisListsTest, SimpleTest2) { |
||||
RedisLists redis(kDefaultDbName, options, true); // Destructive
|
||||
|
||||
string tempv; // Used below for all Index(), PopRight(), PopLeft()
|
||||
|
||||
// Simple PushRight
|
||||
ASSERT_EQ(redis.PushLeft("k1", "v3"), 1); |
||||
ASSERT_EQ(redis.PushLeft("k1", "v2"), 2); |
||||
ASSERT_EQ(redis.PushLeft("k1", "v1"), 3); |
||||
|
||||
// Check Length and Index() functions
|
||||
ASSERT_EQ(redis.Length("k1"), 3); // Check length
|
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "v1"); // Check valid indices
|
||||
ASSERT_TRUE(redis.Index("k1", 1, &tempv)); |
||||
ASSERT_EQ(tempv, "v2"); |
||||
ASSERT_TRUE(redis.Index("k1", 2, &tempv)); |
||||
ASSERT_EQ(tempv, "v3"); |
||||
|
||||
// Check range function and vectors
|
||||
std::vector<std::string> result = redis.Range("k1", 0, 2); // Get the list
|
||||
std::vector<std::string> expected_result(3); |
||||
expected_result[0] = "v1"; |
||||
expected_result[1] = "v2"; |
||||
expected_result[2] = "v3"; |
||||
ASSERT_EQ(result, expected_result); // Uses my overloaded operator==() above
|
||||
} |
||||
|
||||
// Exhaustive test of the Index() function
|
||||
TEST(RedisListsTest, IndexTest) { |
||||
RedisLists redis(kDefaultDbName, options, true); // Destructive
|
||||
|
||||
string tempv; // Used below for all Index(), PopRight(), PopLeft()
|
||||
|
||||
// Empty Index check (return empty and should not crash or edit tempv)
|
||||
tempv = "yo"; |
||||
ASSERT_TRUE(!redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "yo"); |
||||
ASSERT_TRUE(!redis.Index("fda", 3, &tempv)); |
||||
ASSERT_EQ(tempv, "yo"); |
||||
ASSERT_TRUE(!redis.Index("random", -12391, &tempv)); |
||||
ASSERT_EQ(tempv, "yo"); |
||||
|
||||
// Simple Pushes (will yield: [v6, v4, v4, v1, v2, v3]
|
||||
redis.PushRight("k1", "v1"); |
||||
redis.PushRight("k1", "v2"); |
||||
redis.PushRight("k1", "v3"); |
||||
redis.PushLeft("k1", "v4"); |
||||
redis.PushLeft("k1", "v4"); |
||||
redis.PushLeft("k1", "v6"); |
||||
|
||||
// Simple, non-negative indices
|
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "v6"); |
||||
ASSERT_TRUE(redis.Index("k1", 1, &tempv)); |
||||
ASSERT_EQ(tempv, "v4"); |
||||
ASSERT_TRUE(redis.Index("k1", 2, &tempv)); |
||||
ASSERT_EQ(tempv, "v4"); |
||||
ASSERT_TRUE(redis.Index("k1", 3, &tempv)); |
||||
ASSERT_EQ(tempv, "v1"); |
||||
ASSERT_TRUE(redis.Index("k1", 4, &tempv)); |
||||
ASSERT_EQ(tempv, "v2"); |
||||
ASSERT_TRUE(redis.Index("k1", 5, &tempv)); |
||||
ASSERT_EQ(tempv, "v3"); |
||||
|
||||
// Negative indices
|
||||
ASSERT_TRUE(redis.Index("k1", -6, &tempv)); |
||||
ASSERT_EQ(tempv, "v6"); |
||||
ASSERT_TRUE(redis.Index("k1", -5, &tempv)); |
||||
ASSERT_EQ(tempv, "v4"); |
||||
ASSERT_TRUE(redis.Index("k1", -4, &tempv)); |
||||
ASSERT_EQ(tempv, "v4"); |
||||
ASSERT_TRUE(redis.Index("k1", -3, &tempv)); |
||||
ASSERT_EQ(tempv, "v1"); |
||||
ASSERT_TRUE(redis.Index("k1", -2, &tempv)); |
||||
ASSERT_EQ(tempv, "v2"); |
||||
ASSERT_TRUE(redis.Index("k1", -1, &tempv)); |
||||
ASSERT_EQ(tempv, "v3"); |
||||
|
||||
// Out of bounds (return empty, no crash)
|
||||
ASSERT_TRUE(!redis.Index("k1", 6, &tempv)); |
||||
ASSERT_TRUE(!redis.Index("k1", 123219, &tempv)); |
||||
ASSERT_TRUE(!redis.Index("k1", -7, &tempv)); |
||||
ASSERT_TRUE(!redis.Index("k1", -129, &tempv)); |
||||
} |
||||
|
||||
|
||||
// Exhaustive test of the Range() function
|
||||
TEST(RedisListsTest, RangeTest) { |
||||
RedisLists redis(kDefaultDbName, options, true); // Destructive
|
||||
|
||||
string tempv; // Used below for all Index(), PopRight(), PopLeft()
|
||||
|
||||
// Simple Pushes (will yield: [v6, v4, v4, v1, v2, v3])
|
||||
redis.PushRight("k1", "v1"); |
||||
redis.PushRight("k1", "v2"); |
||||
redis.PushRight("k1", "v3"); |
||||
redis.PushLeft("k1", "v4"); |
||||
redis.PushLeft("k1", "v4"); |
||||
redis.PushLeft("k1", "v6"); |
||||
|
||||
// Sanity check (check the length; make sure it's 6)
|
||||
ASSERT_EQ(redis.Length("k1"), 6); |
||||
|
||||
// Simple range
|
||||
std::vector<std::string> res = redis.Range("k1", 1, 4); |
||||
ASSERT_EQ((int)res.size(), 4); |
||||
ASSERT_EQ(res[0], "v4"); |
||||
ASSERT_EQ(res[1], "v4"); |
||||
ASSERT_EQ(res[2], "v1"); |
||||
ASSERT_EQ(res[3], "v2"); |
||||
|
||||
// Negative indices (i.e.: measured from the end)
|
||||
res = redis.Range("k1", 2, -1); |
||||
ASSERT_EQ((int)res.size(), 4); |
||||
ASSERT_EQ(res[0], "v4"); |
||||
ASSERT_EQ(res[1], "v1"); |
||||
ASSERT_EQ(res[2], "v2"); |
||||
ASSERT_EQ(res[3], "v3"); |
||||
|
||||
res = redis.Range("k1", -6, -4); |
||||
ASSERT_EQ((int)res.size(), 3); |
||||
ASSERT_EQ(res[0], "v6"); |
||||
ASSERT_EQ(res[1], "v4"); |
||||
ASSERT_EQ(res[2], "v4"); |
||||
|
||||
res = redis.Range("k1", -1, 5); |
||||
ASSERT_EQ((int)res.size(), 1); |
||||
ASSERT_EQ(res[0], "v3"); |
||||
|
||||
// Partial / Broken indices
|
||||
res = redis.Range("k1", -3, 1000000); |
||||
ASSERT_EQ((int)res.size(), 3); |
||||
ASSERT_EQ(res[0], "v1"); |
||||
ASSERT_EQ(res[1], "v2"); |
||||
ASSERT_EQ(res[2], "v3"); |
||||
|
||||
res = redis.Range("k1", -1000000, 1); |
||||
ASSERT_EQ((int)res.size(), 2); |
||||
ASSERT_EQ(res[0], "v6"); |
||||
ASSERT_EQ(res[1], "v4"); |
||||
|
||||
// Invalid indices
|
||||
res = redis.Range("k1", 7, 9); |
||||
ASSERT_EQ((int)res.size(), 0); |
||||
|
||||
res = redis.Range("k1", -8, -7); |
||||
ASSERT_EQ((int)res.size(), 0); |
||||
|
||||
res = redis.Range("k1", 3, 2); |
||||
ASSERT_EQ((int)res.size(), 0); |
||||
|
||||
res = redis.Range("k1", 5, -2); |
||||
ASSERT_EQ((int)res.size(), 0); |
||||
|
||||
// Range matches Index
|
||||
res = redis.Range("k1", -6, -4); |
||||
ASSERT_TRUE(redis.Index("k1", -6, &tempv)); |
||||
ASSERT_EQ(tempv, res[0]); |
||||
ASSERT_TRUE(redis.Index("k1", -5, &tempv)); |
||||
ASSERT_EQ(tempv, res[1]); |
||||
ASSERT_TRUE(redis.Index("k1", -4, &tempv)); |
||||
ASSERT_EQ(tempv, res[2]); |
||||
|
||||
// Last check
|
||||
res = redis.Range("k1", 0, -6); |
||||
ASSERT_EQ((int)res.size(), 1); |
||||
ASSERT_EQ(res[0], "v6"); |
||||
} |
||||
|
||||
// Exhaustive test for InsertBefore(), and InsertAfter()
|
||||
TEST(RedisListsTest, InsertTest) { |
||||
RedisLists redis(kDefaultDbName, options, true); |
||||
|
||||
string tempv; // Used below for all Index(), PopRight(), PopLeft()
|
||||
|
||||
// Insert on empty list (return 0, and do not crash)
|
||||
ASSERT_EQ(redis.InsertBefore("k1", "non-exist", "a"), 0); |
||||
ASSERT_EQ(redis.InsertAfter("k1", "other-non-exist", "c"), 0); |
||||
ASSERT_EQ(redis.Length("k1"), 0); |
||||
|
||||
// Push some preliminary stuff [g, f, e, d, c, b, a]
|
||||
redis.PushLeft("k1", "a"); |
||||
redis.PushLeft("k1", "b"); |
||||
redis.PushLeft("k1", "c"); |
||||
redis.PushLeft("k1", "d"); |
||||
redis.PushLeft("k1", "e"); |
||||
redis.PushLeft("k1", "f"); |
||||
redis.PushLeft("k1", "g"); |
||||
ASSERT_EQ(redis.Length("k1"), 7); |
||||
|
||||
// Test InsertBefore
|
||||
int newLength = redis.InsertBefore("k1", "e", "hello"); |
||||
ASSERT_EQ(newLength, 8); |
||||
ASSERT_EQ(redis.Length("k1"), newLength); |
||||
ASSERT_TRUE(redis.Index("k1", 1, &tempv)); |
||||
ASSERT_EQ(tempv, "f"); |
||||
ASSERT_TRUE(redis.Index("k1", 3, &tempv)); |
||||
ASSERT_EQ(tempv, "e"); |
||||
ASSERT_TRUE(redis.Index("k1", 2, &tempv)); |
||||
ASSERT_EQ(tempv, "hello"); |
||||
|
||||
// Test InsertAfter
|
||||
newLength = redis.InsertAfter("k1", "c", "bye"); |
||||
ASSERT_EQ(newLength, 9); |
||||
ASSERT_EQ(redis.Length("k1"), newLength); |
||||
ASSERT_TRUE(redis.Index("k1", 6, &tempv)); |
||||
ASSERT_EQ(tempv, "bye"); |
||||
|
||||
// Test bad value on InsertBefore
|
||||
newLength = redis.InsertBefore("k1", "yo", "x"); |
||||
ASSERT_EQ(newLength, 9); |
||||
ASSERT_EQ(redis.Length("k1"), newLength); |
||||
|
||||
// Test bad value on InsertAfter
|
||||
newLength = redis.InsertAfter("k1", "xxxx", "y"); |
||||
ASSERT_EQ(newLength, 9); |
||||
ASSERT_EQ(redis.Length("k1"), newLength); |
||||
|
||||
// Test InsertBefore beginning
|
||||
newLength = redis.InsertBefore("k1", "g", "begggggggggggggggg"); |
||||
ASSERT_EQ(newLength, 10); |
||||
ASSERT_EQ(redis.Length("k1"), newLength); |
||||
|
||||
// Test InsertAfter end
|
||||
newLength = redis.InsertAfter("k1", "a", "enddd"); |
||||
ASSERT_EQ(newLength, 11); |
||||
ASSERT_EQ(redis.Length("k1"), newLength); |
||||
|
||||
// Make sure nothing weird happened.
|
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "begggggggggggggggg"); |
||||
ASSERT_TRUE(redis.Index("k1", 1, &tempv)); |
||||
ASSERT_EQ(tempv, "g"); |
||||
ASSERT_TRUE(redis.Index("k1", 2, &tempv)); |
||||
ASSERT_EQ(tempv, "f"); |
||||
ASSERT_TRUE(redis.Index("k1", 3, &tempv)); |
||||
ASSERT_EQ(tempv, "hello"); |
||||
ASSERT_TRUE(redis.Index("k1", 4, &tempv)); |
||||
ASSERT_EQ(tempv, "e"); |
||||
ASSERT_TRUE(redis.Index("k1", 5, &tempv)); |
||||
ASSERT_EQ(tempv, "d"); |
||||
ASSERT_TRUE(redis.Index("k1", 6, &tempv)); |
||||
ASSERT_EQ(tempv, "c"); |
||||
ASSERT_TRUE(redis.Index("k1", 7, &tempv)); |
||||
ASSERT_EQ(tempv, "bye"); |
||||
ASSERT_TRUE(redis.Index("k1", 8, &tempv)); |
||||
ASSERT_EQ(tempv, "b"); |
||||
ASSERT_TRUE(redis.Index("k1", 9, &tempv)); |
||||
ASSERT_EQ(tempv, "a"); |
||||
ASSERT_TRUE(redis.Index("k1", 10, &tempv)); |
||||
ASSERT_EQ(tempv, "enddd"); |
||||
} |
||||
|
||||
// Exhaustive test of Set function
|
||||
TEST(RedisListsTest, SetTest) { |
||||
RedisLists redis(kDefaultDbName, options, true); |
||||
|
||||
string tempv; // Used below for all Index(), PopRight(), PopLeft()
|
||||
|
||||
// Set on empty list (return false, and do not crash)
|
||||
ASSERT_EQ(redis.Set("k1", 7, "a"), false); |
||||
ASSERT_EQ(redis.Set("k1", 0, "a"), false); |
||||
ASSERT_EQ(redis.Set("k1", -49, "cx"), false); |
||||
ASSERT_EQ(redis.Length("k1"), 0); |
||||
|
||||
// Push some preliminary stuff [g, f, e, d, c, b, a]
|
||||
redis.PushLeft("k1", "a"); |
||||
redis.PushLeft("k1", "b"); |
||||
redis.PushLeft("k1", "c"); |
||||
redis.PushLeft("k1", "d"); |
||||
redis.PushLeft("k1", "e"); |
||||
redis.PushLeft("k1", "f"); |
||||
redis.PushLeft("k1", "g"); |
||||
ASSERT_EQ(redis.Length("k1"), 7); |
||||
|
||||
// Test Regular Set
|
||||
ASSERT_TRUE(redis.Set("k1", 0, "0")); |
||||
ASSERT_TRUE(redis.Set("k1", 3, "3")); |
||||
ASSERT_TRUE(redis.Set("k1", 6, "6")); |
||||
ASSERT_TRUE(redis.Set("k1", 2, "2")); |
||||
ASSERT_TRUE(redis.Set("k1", 5, "5")); |
||||
ASSERT_TRUE(redis.Set("k1", 1, "1")); |
||||
ASSERT_TRUE(redis.Set("k1", 4, "4")); |
||||
|
||||
ASSERT_EQ(redis.Length("k1"), 7); // Size should not change
|
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "0"); |
||||
ASSERT_TRUE(redis.Index("k1", 1, &tempv)); |
||||
ASSERT_EQ(tempv, "1"); |
||||
ASSERT_TRUE(redis.Index("k1", 2, &tempv)); |
||||
ASSERT_EQ(tempv, "2"); |
||||
ASSERT_TRUE(redis.Index("k1", 3, &tempv)); |
||||
ASSERT_EQ(tempv, "3"); |
||||
ASSERT_TRUE(redis.Index("k1", 4, &tempv)); |
||||
ASSERT_EQ(tempv, "4"); |
||||
ASSERT_TRUE(redis.Index("k1", 5, &tempv)); |
||||
ASSERT_EQ(tempv, "5"); |
||||
ASSERT_TRUE(redis.Index("k1", 6, &tempv)); |
||||
ASSERT_EQ(tempv, "6"); |
||||
|
||||
// Set with negative indices
|
||||
ASSERT_TRUE(redis.Set("k1", -7, "a")); |
||||
ASSERT_TRUE(redis.Set("k1", -4, "d")); |
||||
ASSERT_TRUE(redis.Set("k1", -1, "g")); |
||||
ASSERT_TRUE(redis.Set("k1", -5, "c")); |
||||
ASSERT_TRUE(redis.Set("k1", -2, "f")); |
||||
ASSERT_TRUE(redis.Set("k1", -6, "b")); |
||||
ASSERT_TRUE(redis.Set("k1", -3, "e")); |
||||
|
||||
ASSERT_EQ(redis.Length("k1"), 7); // Size should not change
|
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "a"); |
||||
ASSERT_TRUE(redis.Index("k1", 1, &tempv)); |
||||
ASSERT_EQ(tempv, "b"); |
||||
ASSERT_TRUE(redis.Index("k1", 2, &tempv)); |
||||
ASSERT_EQ(tempv, "c"); |
||||
ASSERT_TRUE(redis.Index("k1", 3, &tempv)); |
||||
ASSERT_EQ(tempv, "d"); |
||||
ASSERT_TRUE(redis.Index("k1", 4, &tempv)); |
||||
ASSERT_EQ(tempv, "e"); |
||||
ASSERT_TRUE(redis.Index("k1", 5, &tempv)); |
||||
ASSERT_EQ(tempv, "f"); |
||||
ASSERT_TRUE(redis.Index("k1", 6, &tempv)); |
||||
ASSERT_EQ(tempv, "g"); |
||||
|
||||
// Bad indices (just out-of-bounds / off-by-one check)
|
||||
ASSERT_EQ(redis.Set("k1", -8, "off-by-one in negative index"), false); |
||||
ASSERT_EQ(redis.Set("k1", 7, "off-by-one-error in positive index"), false); |
||||
ASSERT_EQ(redis.Set("k1", 43892, "big random index should fail"), false); |
||||
ASSERT_EQ(redis.Set("k1", -21391, "large negative index should fail"), false); |
||||
|
||||
// One last check (to make sure nothing weird happened)
|
||||
ASSERT_EQ(redis.Length("k1"), 7); // Size should not change
|
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "a"); |
||||
ASSERT_TRUE(redis.Index("k1", 1, &tempv)); |
||||
ASSERT_EQ(tempv, "b"); |
||||
ASSERT_TRUE(redis.Index("k1", 2, &tempv)); |
||||
ASSERT_EQ(tempv, "c"); |
||||
ASSERT_TRUE(redis.Index("k1", 3, &tempv)); |
||||
ASSERT_EQ(tempv, "d"); |
||||
ASSERT_TRUE(redis.Index("k1", 4, &tempv)); |
||||
ASSERT_EQ(tempv, "e"); |
||||
ASSERT_TRUE(redis.Index("k1", 5, &tempv)); |
||||
ASSERT_EQ(tempv, "f"); |
||||
ASSERT_TRUE(redis.Index("k1", 6, &tempv)); |
||||
ASSERT_EQ(tempv, "g"); |
||||
} |
||||
|
||||
// Testing Insert, Push, and Set, in a mixed environment
|
||||
TEST(RedisListsTest, InsertPushSetTest) { |
||||
RedisLists redis(kDefaultDbName, options, true); // Destructive
|
||||
|
||||
string tempv; // Used below for all Index(), PopRight(), PopLeft()
|
||||
|
||||
// A series of pushes and insertions
|
||||
// Will result in [newbegin, z, a, aftera, x, newend]
|
||||
// Also, check the return value sometimes (should return length)
|
||||
int lengthCheck; |
||||
lengthCheck = redis.PushLeft("k1", "a"); |
||||
ASSERT_EQ(lengthCheck, 1); |
||||
redis.PushLeft("k1", "z"); |
||||
redis.PushRight("k1", "x"); |
||||
lengthCheck = redis.InsertAfter("k1", "a", "aftera"); |
||||
ASSERT_EQ(lengthCheck , 4); |
||||
redis.InsertBefore("k1", "z", "newbegin"); // InsertBefore beginning of list
|
||||
redis.InsertAfter("k1", "x", "newend"); // InsertAfter end of list
|
||||
|
||||
// Check
|
||||
std::vector<std::string> res = redis.Range("k1", 0, -1); // Get the list
|
||||
ASSERT_EQ((int)res.size(), 6); |
||||
ASSERT_EQ(res[0], "newbegin"); |
||||
ASSERT_EQ(res[5], "newend"); |
||||
ASSERT_EQ(res[3], "aftera"); |
||||
|
||||
// Testing duplicate values/pivots (multiple occurrences of 'a')
|
||||
ASSERT_TRUE(redis.Set("k1", 0, "a")); // [a, z, a, aftera, x, newend]
|
||||
redis.InsertAfter("k1", "a", "happy"); // [a, happy, z, a, aftera, ...]
|
||||
ASSERT_TRUE(redis.Index("k1", 1, &tempv)); |
||||
ASSERT_EQ(tempv, "happy"); |
||||
redis.InsertBefore("k1", "a", "sad"); // [sad, a, happy, z, a, aftera, ...]
|
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "sad"); |
||||
ASSERT_TRUE(redis.Index("k1", 2, &tempv)); |
||||
ASSERT_EQ(tempv, "happy"); |
||||
ASSERT_TRUE(redis.Index("k1", 5, &tempv)); |
||||
ASSERT_EQ(tempv, "aftera"); |
||||
redis.InsertAfter("k1", "a", "zz"); // [sad, a, zz, happy, z, a, aftera, ...]
|
||||
ASSERT_TRUE(redis.Index("k1", 2, &tempv)); |
||||
ASSERT_EQ(tempv, "zz"); |
||||
ASSERT_TRUE(redis.Index("k1", 6, &tempv)); |
||||
ASSERT_EQ(tempv, "aftera"); |
||||
ASSERT_TRUE(redis.Set("k1", 1, "nota")); // [sad, nota, zz, happy, z, a, ...]
|
||||
redis.InsertBefore("k1", "a", "ba"); // [sad, nota, zz, happy, z, ba, a, ...]
|
||||
ASSERT_TRUE(redis.Index("k1", 4, &tempv)); |
||||
ASSERT_EQ(tempv, "z"); |
||||
ASSERT_TRUE(redis.Index("k1", 5, &tempv)); |
||||
ASSERT_EQ(tempv, "ba"); |
||||
ASSERT_TRUE(redis.Index("k1", 6, &tempv)); |
||||
ASSERT_EQ(tempv, "a"); |
||||
|
||||
// We currently have: [sad, nota, zz, happy, z, ba, a, aftera, x, newend]
|
||||
// redis.Print("k1"); // manually check
|
||||
|
||||
// Test Inserting before/after non-existent values
|
||||
lengthCheck = redis.Length("k1"); // Ensure that the length doesnt change
|
||||
ASSERT_EQ(lengthCheck, 10); |
||||
ASSERT_EQ(redis.InsertBefore("k1", "non-exist", "randval"), lengthCheck); |
||||
ASSERT_EQ(redis.InsertAfter("k1", "nothing", "a"), lengthCheck); |
||||
ASSERT_EQ(redis.InsertAfter("randKey", "randVal", "ranValue"), 0); // Empty
|
||||
ASSERT_EQ(redis.Length("k1"), lengthCheck); // The length should not change
|
||||
|
||||
// Simply Test the Set() function
|
||||
redis.Set("k1", 5, "ba2"); |
||||
redis.InsertBefore("k1", "ba2", "beforeba2"); |
||||
ASSERT_TRUE(redis.Index("k1", 4, &tempv)); |
||||
ASSERT_EQ(tempv, "z"); |
||||
ASSERT_TRUE(redis.Index("k1", 5, &tempv)); |
||||
ASSERT_EQ(tempv, "beforeba2"); |
||||
ASSERT_TRUE(redis.Index("k1", 6, &tempv)); |
||||
ASSERT_EQ(tempv, "ba2"); |
||||
ASSERT_TRUE(redis.Index("k1", 7, &tempv)); |
||||
ASSERT_EQ(tempv, "a"); |
||||
|
||||
// We have: [sad, nota, zz, happy, z, beforeba2, ba2, a, aftera, x, newend]
|
||||
|
||||
// Set() with negative indices
|
||||
redis.Set("k1", -1, "endprank"); |
||||
ASSERT_TRUE(!redis.Index("k1", 11, &tempv)); |
||||
ASSERT_TRUE(redis.Index("k1", 10, &tempv)); |
||||
ASSERT_EQ(tempv, "endprank"); // Ensure Set worked correctly
|
||||
redis.Set("k1", -11, "t"); |
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "t"); |
||||
|
||||
// Test out of bounds Set
|
||||
ASSERT_EQ(redis.Set("k1", -12, "ssd"), false); |
||||
ASSERT_EQ(redis.Set("k1", 11, "sasd"), false); |
||||
ASSERT_EQ(redis.Set("k1", 1200, "big"), false); |
||||
} |
||||
|
||||
// Testing Trim, Pop
|
||||
TEST(RedisListsTest, TrimPopTest) { |
||||
RedisLists redis(kDefaultDbName, options, true); // Destructive
|
||||
|
||||
string tempv; // Used below for all Index(), PopRight(), PopLeft()
|
||||
|
||||
// A series of pushes and insertions
|
||||
// Will result in [newbegin, z, a, aftera, x, newend]
|
||||
redis.PushLeft("k1", "a"); |
||||
redis.PushLeft("k1", "z"); |
||||
redis.PushRight("k1", "x"); |
||||
redis.InsertBefore("k1", "z", "newbegin"); // InsertBefore start of list
|
||||
redis.InsertAfter("k1", "x", "newend"); // InsertAfter end of list
|
||||
redis.InsertAfter("k1", "a", "aftera"); |
||||
|
||||
// Simple PopLeft/Right test
|
||||
ASSERT_TRUE(redis.PopLeft("k1", &tempv)); |
||||
ASSERT_EQ(tempv, "newbegin"); |
||||
ASSERT_EQ(redis.Length("k1"), 5); |
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "z"); |
||||
ASSERT_TRUE(redis.PopRight("k1", &tempv)); |
||||
ASSERT_EQ(tempv, "newend"); |
||||
ASSERT_EQ(redis.Length("k1"), 4); |
||||
ASSERT_TRUE(redis.Index("k1", -1, &tempv)); |
||||
ASSERT_EQ(tempv, "x"); |
||||
|
||||
// Now have: [z, a, aftera, x]
|
||||
|
||||
// Test Trim
|
||||
ASSERT_TRUE(redis.Trim("k1", 0, -1)); // [z, a, aftera, x] (do nothing)
|
||||
ASSERT_EQ(redis.Length("k1"), 4); |
||||
ASSERT_TRUE(redis.Trim("k1", 0, 2)); // [z, a, aftera]
|
||||
ASSERT_EQ(redis.Length("k1"), 3); |
||||
ASSERT_TRUE(redis.Index("k1", -1, &tempv)); |
||||
ASSERT_EQ(tempv, "aftera"); |
||||
ASSERT_TRUE(redis.Trim("k1", 1, 1)); // [a]
|
||||
ASSERT_EQ(redis.Length("k1"), 1); |
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "a"); |
||||
|
||||
// Test out of bounds (empty) trim
|
||||
ASSERT_TRUE(redis.Trim("k1", 1, 0)); |
||||
ASSERT_EQ(redis.Length("k1"), 0); |
||||
|
||||
// Popping with empty list (return empty without error)
|
||||
ASSERT_TRUE(!redis.PopLeft("k1", &tempv)); |
||||
ASSERT_TRUE(!redis.PopRight("k1", &tempv)); |
||||
ASSERT_TRUE(redis.Trim("k1", 0, 5)); |
||||
|
||||
// Exhaustive Trim test (negative and invalid indices)
|
||||
// Will start in [newbegin, z, a, aftera, x, newend]
|
||||
redis.PushLeft("k1", "a"); |
||||
redis.PushLeft("k1", "z"); |
||||
redis.PushRight("k1", "x"); |
||||
redis.InsertBefore("k1", "z", "newbegin"); // InsertBefore start of list
|
||||
redis.InsertAfter("k1", "x", "newend"); // InsertAfter end of list
|
||||
redis.InsertAfter("k1", "a", "aftera"); |
||||
ASSERT_TRUE(redis.Trim("k1", -6, -1)); // Should do nothing
|
||||
ASSERT_EQ(redis.Length("k1"), 6); |
||||
ASSERT_TRUE(redis.Trim("k1", 1, -2)); |
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "z"); |
||||
ASSERT_TRUE(redis.Index("k1", 3, &tempv)); |
||||
ASSERT_EQ(tempv, "x"); |
||||
ASSERT_EQ(redis.Length("k1"), 4); |
||||
ASSERT_TRUE(redis.Trim("k1", -3, -2)); |
||||
ASSERT_EQ(redis.Length("k1"), 2); |
||||
} |
||||
|
||||
// Testing Remove, RemoveFirst, RemoveLast
|
||||
TEST(RedisListsTest, RemoveTest) { |
||||
RedisLists redis(kDefaultDbName, options, true); // Destructive
|
||||
|
||||
string tempv; // Used below for all Index(), PopRight(), PopLeft()
|
||||
|
||||
// A series of pushes and insertions
|
||||
// Will result in [newbegin, z, a, aftera, x, newend, a, a]
|
||||
redis.PushLeft("k1", "a"); |
||||
redis.PushLeft("k1", "z"); |
||||
redis.PushRight("k1", "x"); |
||||
redis.InsertBefore("k1", "z", "newbegin"); // InsertBefore start of list
|
||||
redis.InsertAfter("k1", "x", "newend"); // InsertAfter end of list
|
||||
redis.InsertAfter("k1", "a", "aftera"); |
||||
redis.PushRight("k1", "a"); |
||||
redis.PushRight("k1", "a"); |
||||
|
||||
// Verify
|
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "newbegin"); |
||||
ASSERT_TRUE(redis.Index("k1", -1, &tempv)); |
||||
ASSERT_EQ(tempv, "a"); |
||||
|
||||
// Check RemoveFirst (Remove the first two 'a')
|
||||
// Results in [newbegin, z, aftera, x, newend, a]
|
||||
int numRemoved = redis.Remove("k1", 2, "a"); |
||||
ASSERT_EQ(numRemoved, 2); |
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "newbegin"); |
||||
ASSERT_TRUE(redis.Index("k1", 1, &tempv)); |
||||
ASSERT_EQ(tempv, "z"); |
||||
ASSERT_TRUE(redis.Index("k1", 4, &tempv)); |
||||
ASSERT_EQ(tempv, "newend"); |
||||
ASSERT_TRUE(redis.Index("k1", 5, &tempv)); |
||||
ASSERT_EQ(tempv, "a"); |
||||
ASSERT_EQ(redis.Length("k1"), 6); |
||||
|
||||
// Repopulate some stuff
|
||||
// Results in: [x, x, x, x, x, newbegin, z, x, aftera, x, newend, a, x]
|
||||
redis.PushLeft("k1", "x"); |
||||
redis.PushLeft("k1", "x"); |
||||
redis.PushLeft("k1", "x"); |
||||
redis.PushLeft("k1", "x"); |
||||
redis.PushLeft("k1", "x"); |
||||
redis.PushRight("k1", "x"); |
||||
redis.InsertAfter("k1", "z", "x"); |
||||
|
||||
// Test removal from end
|
||||
numRemoved = redis.Remove("k1", -2, "x"); |
||||
ASSERT_EQ(numRemoved, 2); |
||||
ASSERT_TRUE(redis.Index("k1", 8, &tempv)); |
||||
ASSERT_EQ(tempv, "aftera"); |
||||
ASSERT_TRUE(redis.Index("k1", 9, &tempv)); |
||||
ASSERT_EQ(tempv, "newend"); |
||||
ASSERT_TRUE(redis.Index("k1", 10, &tempv)); |
||||
ASSERT_EQ(tempv, "a"); |
||||
ASSERT_TRUE(!redis.Index("k1", 11, &tempv)); |
||||
numRemoved = redis.Remove("k1", -2, "x"); |
||||
ASSERT_EQ(numRemoved, 2); |
||||
ASSERT_TRUE(redis.Index("k1", 4, &tempv)); |
||||
ASSERT_EQ(tempv, "newbegin"); |
||||
ASSERT_TRUE(redis.Index("k1", 6, &tempv)); |
||||
ASSERT_EQ(tempv, "aftera"); |
||||
|
||||
// We now have: [x, x, x, x, newbegin, z, aftera, newend, a]
|
||||
ASSERT_EQ(redis.Length("k1"), 9); |
||||
ASSERT_TRUE(redis.Index("k1", -1, &tempv)); |
||||
ASSERT_EQ(tempv, "a"); |
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "x"); |
||||
|
||||
// Test over-shooting (removing more than there exists)
|
||||
numRemoved = redis.Remove("k1", -9000, "x"); |
||||
ASSERT_EQ(numRemoved , 4); // Only really removed 4
|
||||
ASSERT_EQ(redis.Length("k1"), 5); |
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "newbegin"); |
||||
numRemoved = redis.Remove("k1", 1, "x"); |
||||
ASSERT_EQ(numRemoved, 0); |
||||
|
||||
// Try removing ALL!
|
||||
numRemoved = redis.Remove("k1", 0, "newbegin"); // REMOVE 0 will remove all!
|
||||
ASSERT_EQ(numRemoved, 1); |
||||
|
||||
// Removal from an empty-list
|
||||
ASSERT_TRUE(redis.Trim("k1", 1, 0)); |
||||
numRemoved = redis.Remove("k1", 1, "z"); |
||||
ASSERT_EQ(numRemoved, 0); |
||||
} |
||||
|
||||
|
||||
// Test Multiple keys and Persistence
|
||||
TEST(RedisListsTest, PersistenceMultiKeyTest) { |
||||
|
||||
string tempv; // Used below for all Index(), PopRight(), PopLeft()
|
||||
|
||||
// Block one: populate a single key in the database
|
||||
{ |
||||
RedisLists redis(kDefaultDbName, options, true); // Destructive
|
||||
|
||||
// A series of pushes and insertions
|
||||
// Will result in [newbegin, z, a, aftera, x, newend, a, a]
|
||||
redis.PushLeft("k1", "a"); |
||||
redis.PushLeft("k1", "z"); |
||||
redis.PushRight("k1", "x"); |
||||
redis.InsertBefore("k1", "z", "newbegin"); // InsertBefore start of list
|
||||
redis.InsertAfter("k1", "x", "newend"); // InsertAfter end of list
|
||||
redis.InsertAfter("k1", "a", "aftera"); |
||||
redis.PushRight("k1", "a"); |
||||
redis.PushRight("k1", "a"); |
||||
|
||||
ASSERT_TRUE(redis.Index("k1", 3, &tempv)); |
||||
ASSERT_EQ(tempv, "aftera"); |
||||
} |
||||
|
||||
// Block two: make sure changes were saved and add some other key
|
||||
{ |
||||
RedisLists redis(kDefaultDbName, options, false); // Persistent, non-destructive
|
||||
|
||||
// Check
|
||||
ASSERT_EQ(redis.Length("k1"), 8); |
||||
ASSERT_TRUE(redis.Index("k1", 3, &tempv)); |
||||
ASSERT_EQ(tempv, "aftera"); |
||||
|
||||
redis.PushRight("k2", "randomkey"); |
||||
redis.PushLeft("k2", "sas"); |
||||
|
||||
redis.PopLeft("k1", &tempv); |
||||
} |
||||
|
||||
// Block three: Verify the changes from block 2
|
||||
{ |
||||
RedisLists redis(kDefaultDbName, options, false); // Presistent, non-destructive
|
||||
|
||||
// Check
|
||||
ASSERT_EQ(redis.Length("k1"), 7); |
||||
ASSERT_EQ(redis.Length("k2"), 2); |
||||
ASSERT_TRUE(redis.Index("k1", 0, &tempv)); |
||||
ASSERT_EQ(tempv, "z"); |
||||
ASSERT_TRUE(redis.Index("k2", -2, &tempv)); |
||||
ASSERT_EQ(tempv, "sas"); |
||||
} |
||||
} |
||||
|
||||
/// THE manual REDIS TEST begins here
|
||||
/// THIS WILL ONLY OCCUR IF YOU RUN: ./redis_test -m
|
||||
|
||||
void MakeUpper(std::string* const s) { |
||||
int len = s->length(); |
||||
for(int i=0; i<len; ++i) { |
||||
(*s)[i] = toupper((*s)[i]); // C-version defined in <ctype.h>
|
||||
} |
||||
} |
||||
|
||||
/// Allows the user to enter in REDIS commands into the command-line.
|
||||
/// This is useful for manual / interacticve testing / debugging.
|
||||
/// Use destructive=true to clean the database before use.
|
||||
/// Use destructive=false to remember the previous state (i.e.: persistent)
|
||||
/// Should be called from main function.
|
||||
int manual_redis_test(bool destructive){ |
||||
RedisLists redis(RedisListsTest::kDefaultDbName, |
||||
RedisListsTest::options, |
||||
destructive); |
||||
|
||||
// TODO: Right now, please use spaces to separate each word.
|
||||
// In actual redis, you can use quotes to specify compound values
|
||||
// Example: RPUSH mylist "this is a compound value"
|
||||
|
||||
std::string command; |
||||
while(true) { |
||||
cin >> command; |
||||
MakeUpper(&command); |
||||
|
||||
if (command == "LINSERT") { |
||||
std::string k, t, p, v; |
||||
cin >> k >> t >> p >> v; |
||||
MakeUpper(&t); |
||||
if (t=="BEFORE") { |
||||
std::cout << redis.InsertBefore(k, p, v) << std::endl; |
||||
} else if (t=="AFTER") { |
||||
std::cout << redis.InsertAfter(k, p, v) << std::endl; |
||||
} |
||||
} else if (command == "LPUSH") { |
||||
std::string k, v; |
||||
std::cin >> k >> v; |
||||
redis.PushLeft(k, v); |
||||
} else if (command == "RPUSH") { |
||||
std::string k, v; |
||||
std::cin >> k >> v; |
||||
redis.PushRight(k, v); |
||||
} else if (command == "LPOP") { |
||||
std::string k; |
||||
std::cin >> k; |
||||
string res; |
||||
redis.PopLeft(k, &res); |
||||
std::cout << res << std::endl; |
||||
} else if (command == "RPOP") { |
||||
std::string k; |
||||
std::cin >> k; |
||||
string res; |
||||
redis.PopRight(k, &res); |
||||
std::cout << res << std::endl; |
||||
} else if (command == "LREM") { |
||||
std::string k; |
||||
int amt; |
||||
std::string v; |
||||
|
||||
std::cin >> k >> amt >> v; |
||||
std::cout << redis.Remove(k, amt, v) << std::endl; |
||||
} else if (command == "LLEN") { |
||||
std::string k; |
||||
std::cin >> k; |
||||
std::cout << redis.Length(k) << std::endl; |
||||
} else if (command == "LRANGE") { |
||||
std::string k; |
||||
int i, j; |
||||
std::cin >> k >> i >> j; |
||||
std::vector<std::string> res = redis.Range(k, i, j); |
||||
for (auto it = res.begin(); it != res.end(); ++it) { |
||||
std::cout << " " << (*it); |
||||
} |
||||
std::cout << std::endl; |
||||
} else if (command == "LTRIM") { |
||||
std::string k; |
||||
int i, j; |
||||
std::cin >> k >> i >> j; |
||||
redis.Trim(k, i, j); |
||||
} else if (command == "LSET") { |
||||
std::string k; |
||||
int idx; |
||||
std::string v; |
||||
cin >> k >> idx >> v; |
||||
redis.Set(k, idx, v); |
||||
} else if (command == "LINDEX") { |
||||
std::string k; |
||||
int idx; |
||||
std::cin >> k >> idx; |
||||
string res; |
||||
redis.Index(k, idx, &res); |
||||
std::cout << res << std::endl; |
||||
} else if (command == "PRINT") { // Added by Deon
|
||||
std::string k; |
||||
cin >> k; |
||||
redis.Print(k); |
||||
} else if (command == "QUIT") { |
||||
return 0; |
||||
} else { |
||||
std::cout << "unknown command: " << command << std::endl; |
||||
} |
||||
} |
||||
} |
||||
|
||||
} // namespace leveldb
|
||||
|
||||
|
||||
// USAGE: "./redis_test" for default (unit tests)
|
||||
// "./redis_test -m" for manual testing (redis command api)
|
||||
// "./redis_test -m -d" for destructive manual test (erase db before use)
|
||||
|
||||
|
||||
// Check for "want" argument in the argument list
|
||||
bool found_arg(int argc, char* argv[], const char* want){ |
||||
for(int i=1; i<argc; ++i){ |
||||
if (strcmp(argv[i], want) == 0) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
// Will run unit tests.
|
||||
// However, if -m is specified, it will do user manual/interactive testing
|
||||
// -m -d is manual and destructive (will clear the database before use)
|
||||
int main(int argc, char* argv[]) { |
||||
if (found_arg(argc, argv, "-m")) { |
||||
bool destructive = found_arg(argc, argv, "-d"); |
||||
return leveldb::manual_redis_test(destructive); |
||||
} else { |
||||
return leveldb::test::RunAllTests(); |
||||
} |
||||
} |
||||
|
Loading…
Reference in new issue