Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddcb393200 | |||
| f760a5fecd | |||
| a820f587b1 |
@@ -5,6 +5,7 @@
|
||||
#pragma once
|
||||
#include "action.hpp"
|
||||
#include "../../core/eosio/print.hpp"
|
||||
#include "map.hpp"
|
||||
#include "multi_index.hpp"
|
||||
#include "dispatcher.hpp"
|
||||
#include "contract.hpp"
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
#pragma once
|
||||
#include "../../core/eosio/context.hpp"
|
||||
#include "../../core/eosio/datastream.hpp"
|
||||
#include "../../core/eosio/name.hpp"
|
||||
#include "../../core/eosio/varint.hpp"
|
||||
|
||||
#include "../../core/eosio/key_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
|
||||
namespace eosio::kv {
|
||||
namespace internal_use_do_not_use {
|
||||
extern "C" {
|
||||
__attribute__((eosio_wasm_import))
|
||||
int64_t kv_erase(uint64_t contract, const char* key, uint32_t key_size);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
int64_t kv_set(uint64_t contract, const char* key, uint32_t key_size, const char* value, uint32_t value_size, uint64_t payer);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
bool kv_get(uint64_t contract, const char* key, uint32_t key_size, uint32_t& value_size);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
uint32_t kv_get_data(uint32_t offset, char* data, uint32_t data_size);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
uint32_t kv_it_create(uint64_t contract, const char* prefix, uint32_t size);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
void kv_it_destroy(uint32_t itr);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
int32_t kv_it_status(uint32_t itr);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
int32_t kv_it_compare(uint32_t itr_a, uint32_t itr_b);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
int32_t kv_it_key_compare(uint32_t itr, const char* key, uint32_t size);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
int32_t kv_it_move_to_end(uint32_t itr);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
int32_t kv_it_next(uint32_t itr, uint32_t& found_key_size, uint32_t& found_value_size);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
int32_t kv_it_prev(uint32_t itr, uint32_t& found_key_size, uint32_t& found_value_size);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
int32_t kv_it_lower_bound(uint32_t itr, const char* key, uint32_t size, uint32_t& found_key_size, uint32_t& found_value_size);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
int32_t kv_it_key(uint32_t itr, uint32_t offset, char* dest, uint32_t size, uint32_t& actual_size);
|
||||
|
||||
__attribute__((eosio_wasm_import))
|
||||
int32_t kv_it_value(uint32_t itr, uint32_t offset, char* dest, uint32_t size, uint32_t& actual_size);
|
||||
}
|
||||
}
|
||||
|
||||
// tag used by some of the types to delineate overloads between key_type (std::string) and the map's key type
|
||||
struct packed_tag {};
|
||||
|
||||
namespace detail {
|
||||
static inline void increment_bytes(key_type& kt) {
|
||||
// this will terminate without have to lengthen by one byte given the way prefixes are formed
|
||||
// need to re-evaluate if that changes
|
||||
for (std::size_t i=kt.size()-1; i >= 0; i--) {
|
||||
uint16_t v = kt[i]+1;
|
||||
kt[i] = v;
|
||||
if (v <= 0xFF)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
struct packed_view {
|
||||
packed_view() = default;
|
||||
constexpr inline packed_view(char* p, std::size_t s)
|
||||
: ptr(p), sz(s) {}
|
||||
|
||||
using iterator = char*;
|
||||
using const_iterator = const char*;
|
||||
|
||||
constexpr inline char* data() { return ptr; }
|
||||
constexpr inline const char* data() const { return ptr; }
|
||||
constexpr inline std::size_t size() const { return sz; }
|
||||
constexpr inline const_iterator begin() const { return data(); }
|
||||
constexpr inline const_iterator end() const { return data() + size(); }
|
||||
char* ptr;
|
||||
std::size_t sz;
|
||||
};
|
||||
|
||||
inline uint32_t itr_create(name contract, std::string_view prefix) {
|
||||
return internal_use_do_not_use::kv_it_create(contract.value, prefix.data(), prefix.size());
|
||||
}
|
||||
|
||||
inline void itr_destroy(uint32_t itr) { internal_use_do_not_use::kv_it_destroy(itr); }
|
||||
inline int32_t itr_status(uint32_t itr) { return internal_use_do_not_use::kv_it_status(itr); }
|
||||
inline int32_t itr_compare(uint32_t a, uint32_t b) { return internal_use_do_not_use::kv_it_compare(a, b); }
|
||||
inline int32_t itr_key_compare(uint32_t itr, std::string_view k) { return internal_use_do_not_use::kv_it_key_compare(itr, k.data(), k.size()); }
|
||||
inline int32_t itr_move_to_end(uint32_t itr) { return internal_use_do_not_use::kv_it_move_to_end(itr); }
|
||||
inline int32_t itr_key(uint32_t itr, char* dest, uint32_t size, uint32_t& actual_size) { return internal_use_do_not_use::kv_it_key(itr, 0, dest, size, actual_size); }
|
||||
inline int32_t itr_value(uint32_t itr, char* dest, uint32_t size, uint32_t& actual_size) { return internal_use_do_not_use::kv_it_value(itr, 0, dest, size, actual_size); }
|
||||
|
||||
inline int32_t itr_next(uint32_t itr, uint32_t& key_size, uint32_t& val_size) {
|
||||
return internal_use_do_not_use::kv_it_next(itr, key_size, val_size);
|
||||
}
|
||||
inline int32_t itr_next(uint32_t itr) {
|
||||
uint32_t k,v;
|
||||
return internal_use_do_not_use::kv_it_next(itr, k, v);
|
||||
}
|
||||
inline int32_t itr_prev(uint32_t itr, uint32_t& key_size, uint32_t& val_size) {
|
||||
return internal_use_do_not_use::kv_it_prev(itr, key_size, val_size);
|
||||
}
|
||||
inline int32_t itr_prev(uint32_t itr) {
|
||||
uint32_t k,v;
|
||||
return internal_use_do_not_use::kv_it_prev(itr, k, v);
|
||||
}
|
||||
inline int32_t itr_lower_bound(uint32_t itr, std::string_view k, uint32_t& key_size, uint32_t& val_size) {
|
||||
return internal_use_do_not_use::kv_it_lower_bound(itr, k.data(), k.size(), key_size, val_size);
|
||||
}
|
||||
inline int32_t itr_lower_bound(uint32_t itr, std::string_view k) {
|
||||
uint32_t _k,_v;
|
||||
return internal_use_do_not_use::kv_it_lower_bound(itr, k.data(), k.size(), _k, _v);
|
||||
}
|
||||
inline int32_t itr_lower_bound(uint32_t itr) {
|
||||
uint32_t _k,_v;
|
||||
return internal_use_do_not_use::kv_it_lower_bound(itr, "", 0, _k, _v);
|
||||
}
|
||||
|
||||
template <typename KV>
|
||||
struct elem {
|
||||
using key_t = typename KV::key_t;
|
||||
using value_t = typename KV::value_t;
|
||||
elem() = default;
|
||||
constexpr inline elem(const key_t& k, value_t v, name p=current_context_contract())
|
||||
: key(KV::full_key(k)), value(std::move(v)), payer(p) {}
|
||||
|
||||
inline elem(key_type k, value_t v, name p, packed_tag)
|
||||
: key(std::move(k)), value(std::move(v)), payer(p) {}
|
||||
|
||||
constexpr inline bool operator==(const elem& e) const {
|
||||
return std::tie(key, value) == std::tie(e.key, e.value);
|
||||
}
|
||||
|
||||
key_type& first() { return key; }
|
||||
const key_type& first() const { return key; }
|
||||
value_t& second() { return value; }
|
||||
const value_t& second() const { return value; }
|
||||
|
||||
key_type key;
|
||||
value_t value;
|
||||
name payer;
|
||||
};
|
||||
|
||||
template <bool Reverse, typename KV>
|
||||
struct iterator {
|
||||
using elem_t = elem<KV>;
|
||||
using value_t = typename KV::value_t;
|
||||
constexpr static inline uint32_t invalidated_iterator = std::numeric_limits<uint32_t>::max();
|
||||
|
||||
enum class status { ok = 0, erased = -1, end = -2 };
|
||||
|
||||
template <status Stat>
|
||||
constexpr inline static bool query_status(status stat) {
|
||||
using namespace internal_use_do_not_use;
|
||||
return Stat == stat;
|
||||
}
|
||||
template <status Stat>
|
||||
constexpr inline static bool query_status(int32_t stat) {
|
||||
using namespace internal_use_do_not_use;
|
||||
return Stat == static_cast<status>(stat);
|
||||
}
|
||||
|
||||
inline iterator(name owner)
|
||||
: element(),
|
||||
handle(itr_create(owner, {KV::prefix().data(), KV::prefix().size()})) {}
|
||||
|
||||
iterator(const iterator&) = delete;
|
||||
iterator& operator=(const iterator&) = delete;
|
||||
|
||||
inline iterator(iterator&& o)
|
||||
: element(o.element),
|
||||
handle(std::exchange(o.handle, invalidated_iterator)),
|
||||
current_status(o.current_status) {}
|
||||
|
||||
inline iterator(iterator<!Reverse, KV>&& o)
|
||||
: element(o.element),
|
||||
handle(std::exchange(o.handle, invalidated_iterator)),
|
||||
current_status(o.current_status) {}
|
||||
|
||||
inline iterator& operator=(iterator&& o) {
|
||||
element = o.element;
|
||||
if (handle != invalidated_iterator)
|
||||
itr_destroy(handle);
|
||||
handle = std::exchange(o.handle, invalidated_iterator);
|
||||
current_status = o.current_status;
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline iterator& operator=(iterator<!Reverse, KV>&& o) {
|
||||
element = o.element;
|
||||
if (handle != invalidated_iterator)
|
||||
itr_destroy(handle);
|
||||
handle = std::exchange(o.handle, invalidated_iterator);
|
||||
current_status = o.current_status;
|
||||
return *this;
|
||||
}
|
||||
|
||||
~iterator() {
|
||||
if (handle != invalidated_iterator)
|
||||
itr_destroy(handle);
|
||||
}
|
||||
|
||||
inline bool is_valid() const { return query_status<status::ok>(current_status); }
|
||||
|
||||
iterator& seek_to_begin() {
|
||||
current_status = static_cast<status>(itr_lower_bound(handle));
|
||||
return *this;
|
||||
}
|
||||
|
||||
iterator& seek_to_last() {
|
||||
current_status = static_cast<status>(itr_move_to_end(handle));
|
||||
current_status = static_cast<status>(itr_prev(handle));
|
||||
return *this;
|
||||
}
|
||||
|
||||
iterator& seek_to_end() {
|
||||
current_status = static_cast<status>(itr_move_to_end(handle));
|
||||
return *this;
|
||||
}
|
||||
|
||||
iterator& lower_bound(const key_type& k) {
|
||||
current_status = static_cast<status>(itr_lower_bound(handle, {k.data(), k.size()}));
|
||||
return *this;
|
||||
}
|
||||
|
||||
iterator& find(const key_type& k) {
|
||||
lower_bound(k);
|
||||
if (itr_key_compare(handle, {k.data(), k.size()}) != 0)
|
||||
seek_to_end();
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline elem_t& operator*() {
|
||||
materialize();
|
||||
return element;
|
||||
}
|
||||
inline const elem_t& operator*() const {
|
||||
materialize();
|
||||
return element;
|
||||
}
|
||||
inline elem_t* operator->() {
|
||||
materialize();
|
||||
return &element;
|
||||
}
|
||||
inline const elem_t* operator->() const {
|
||||
materialize();
|
||||
return &element;
|
||||
}
|
||||
|
||||
iterator& operator++() {
|
||||
if constexpr (Reverse) {
|
||||
current_status = static_cast<status>(itr_prev(handle));
|
||||
check(query_status<status::ok>(current_status), "incrementing past end or an erased iterator");
|
||||
} else {
|
||||
check(query_status<status::ok>(current_status), "incrementing past end or an erased iterator");
|
||||
current_status = static_cast<status>(itr_next(handle));
|
||||
}
|
||||
if (query_status<status::end>(current_status))
|
||||
seek_to_end();
|
||||
return *this;
|
||||
}
|
||||
|
||||
iterator& operator--() {
|
||||
if constexpr (Reverse) {
|
||||
check(query_status<status::ok>(current_status), "decrementing past end or an erased iterator");
|
||||
current_status = static_cast<status>(itr_next(handle));
|
||||
} else {
|
||||
current_status = static_cast<status>(itr_prev(handle));
|
||||
check(query_status<status::ok>(current_status), "decrementing past end or an erased iterator");
|
||||
}
|
||||
if (query_status<status::end>(current_status))
|
||||
seek_to_end();
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline bool operator==(const iterator& o) const {
|
||||
// ignoring key_size and value_size as they shouldn't play a role in equality
|
||||
return (std::tie(handle, current_status) == std::tie(o.handle, o.current_status)) ||
|
||||
(query_status<status::end>(current_status) && query_status<status::end>(o.current_status)) ||
|
||||
!itr_compare(handle, o.handle);
|
||||
}
|
||||
|
||||
inline bool operator!=(const iterator& o) const { return !((*this) == o); }
|
||||
|
||||
void materialize() {
|
||||
using namespace internal_use_do_not_use;
|
||||
uint32_t sz;
|
||||
itr_key(handle, nullptr, 0, sz);
|
||||
element.key.resize(sz);
|
||||
check(query_status<status::ok>(itr_key(handle, element.key.data(), element.key.size(), sz)), "failure getting key");
|
||||
|
||||
itr_value(handle, nullptr, 0, sz);
|
||||
auto val_bytes = KV::get_tmp_buffer(sz);
|
||||
auto status = itr_value(handle, val_bytes.data(), val_bytes.size(), sz);
|
||||
check(query_status<status::ok>(status), "failure getting value");
|
||||
unpack<value_t>(element.value, val_bytes.data(), val_bytes.size());
|
||||
}
|
||||
|
||||
elem_t element;
|
||||
uint32_t handle;
|
||||
status current_status = status::ok;
|
||||
};
|
||||
} // namespace eosio::kv::detail
|
||||
|
||||
template <eosio::name::raw TableName, typename K, typename V, eosio::name::raw IndexName="map.index"_n>
|
||||
class [[eosio::table]] map {
|
||||
public:
|
||||
constexpr static inline uint8_t magic = 1;
|
||||
constexpr static inline name table_name = name{static_cast<uint64_t>(TableName)};
|
||||
using key_t = K;
|
||||
using value_t = V;
|
||||
using self_t = map<TableName, K, V>;
|
||||
|
||||
static const key_type& prefix() {
|
||||
static key_type prfx = eosio::detail::const_pack(table_name, index_name, magic);
|
||||
return prfx;
|
||||
}
|
||||
|
||||
static key_type full_key(const key_t& k) { return prefix() + convert_to_key(k); }
|
||||
|
||||
using elem_t = detail::elem<self_t>;
|
||||
using iterator_t = detail::iterator<false, self_t>;
|
||||
using reverse_iterator_t = detail::iterator<true, self_t>;
|
||||
|
||||
struct writable_wrapper {
|
||||
writable_wrapper(key_type k, value_t v, name p, name o=current_context_contract())
|
||||
: element(std::move(k), std::move(v), p, packed_tag{}), owner(o) {}
|
||||
|
||||
explicit operator value_t&() { return element.value; }
|
||||
operator value_t() const { return element.value; }
|
||||
|
||||
writable_wrapper& operator=(const value_t& o) {
|
||||
map{owner}.set(element.key, o, element.payer, packed_tag{});
|
||||
element.value = o;
|
||||
return *this;
|
||||
}
|
||||
|
||||
writable_wrapper& operator=(value_t&& o) {
|
||||
map{owner}.set(element.key, o, element.payer, packed_tag{});
|
||||
element.value = std::move(o);
|
||||
return *this;
|
||||
}
|
||||
|
||||
elem_t element;
|
||||
name owner;
|
||||
};
|
||||
|
||||
inline map(name owner=current_context_contract())
|
||||
: owner(owner) {}
|
||||
|
||||
/**
|
||||
* Basic constructor of N elements. This will bill the implicit owner of the table by default.
|
||||
*/
|
||||
inline map(std::initializer_list<elem_t> l) {
|
||||
for ( const auto& e : l ) {
|
||||
set(e.key, e.value, e.payer, packed_tag{});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor of N elements. This will bill the owner explicitly passed in.
|
||||
*/
|
||||
inline map(name owner, std::initializer_list<elem_t> l)
|
||||
: owner(owner) {
|
||||
for ( const auto& e : l ) {
|
||||
set(e.key, e.value, e.payer, packed_tag{});
|
||||
}
|
||||
}
|
||||
|
||||
writable_wrapper operator[](const std::pair<key_t, name>& key_payer) {
|
||||
auto v = get(key_payer.first);
|
||||
if (std::get<0>(v))
|
||||
return {std::get<1>(v), *std::get<0>(v), key_payer.second, owner};
|
||||
|
||||
set(std::get<1>(v), value_t{}, owner, packed_tag{});
|
||||
return {std::move(std::get<1>(v)), value_t{}, key_payer.second, owner};
|
||||
}
|
||||
|
||||
writable_wrapper operator[](const key_t& k) {
|
||||
return (*this)[std::pair{k, owner}];
|
||||
}
|
||||
|
||||
template <typename Key>
|
||||
writable_wrapper at(Key&& k, name payer=current_context_contract()) {
|
||||
auto v = get(std::forward<Key>(k));
|
||||
check(std::get<0>(v), "key not found");
|
||||
return {std::get<1>(v), *std::get<0>(v), payer, owner};
|
||||
}
|
||||
|
||||
inline iterator_t begin() const {
|
||||
iterator_t it = {owner};
|
||||
it.seek_to_begin();
|
||||
return it;
|
||||
}
|
||||
|
||||
inline iterator_t end() const {
|
||||
iterator_t it = {owner};
|
||||
it.seek_to_end();
|
||||
return it;
|
||||
}
|
||||
|
||||
inline reverse_iterator_t rbegin() const {
|
||||
reverse_iterator_t it = {owner};
|
||||
it.seek_to_last();
|
||||
return it;
|
||||
}
|
||||
|
||||
inline reverse_iterator_t rend() const {
|
||||
return end();
|
||||
}
|
||||
|
||||
inline bool empty() const {
|
||||
auto it = lower_bound("");
|
||||
return it == end();
|
||||
}
|
||||
|
||||
iterator_t find(const key_t& k) const {
|
||||
auto fk = full_key(k);
|
||||
iterator_t it = {owner};
|
||||
it.find(fk);
|
||||
return it;
|
||||
}
|
||||
|
||||
void erase(const key_t& k) {
|
||||
using namespace internal_use_do_not_use;
|
||||
auto fk = full_key(k);
|
||||
kv_erase(owner.value, fk.data(), fk.size());
|
||||
}
|
||||
|
||||
iterator_t erase(const iterator_t& it) {
|
||||
using namespace internal_use_do_not_use;
|
||||
auto key = *it.key;
|
||||
++it;
|
||||
kv_erase(owner, key.data(), key.size());
|
||||
return it;
|
||||
}
|
||||
|
||||
inline iterator_t lower_bound(const key_t& k) const {
|
||||
iterator_t it = {owner};
|
||||
it.lower_bound(full_key(k));
|
||||
return it;
|
||||
}
|
||||
|
||||
inline iterator_t upper_bound(const key_t& k) const {
|
||||
auto fk = full_key(k);
|
||||
detail::increment_bytes(fk); // add '1' to the last byte, this should get us to the next value up
|
||||
iterator_t it = {owner};
|
||||
it.lower_bound(fk);
|
||||
return it;
|
||||
}
|
||||
|
||||
std::pair<iterator_t, iterator_t> equal_range(const key_t& k) const { return {lower_bound(k), upper_bound(k)}; }
|
||||
|
||||
std::vector<elem_t> ranged_slice(const key_t& l, const key_t& h) {
|
||||
std::vector<elem_t> ret;
|
||||
|
||||
for (auto itr = lower_bound(l); itr != lower_bound(h); ++itr) {
|
||||
ret.emplace_back(std::move(*itr));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline bool contains(const key_t& k) const {
|
||||
using namespace internal_use_do_not_use;
|
||||
uint32_t _vs;
|
||||
auto fk = full_key(k);
|
||||
return static_cast<bool>(kv_get(owner.value, fk.data(), fk.size(), _vs));
|
||||
}
|
||||
|
||||
bool raw_write(const key_t& k, std::string_view bytes, name payer=current_context_contract()) const {
|
||||
using namespace internal_use_do_not_use;
|
||||
const auto& fk = full_key(k);
|
||||
auto wrote = kv_set(owner.value, fk.data(), fk.size(), bytes.data(), bytes.size(), payer.value);
|
||||
return wrote == bytes.size();
|
||||
}
|
||||
|
||||
friend iterator_t;
|
||||
|
||||
protected:
|
||||
constexpr static inline name index_name = name{IndexName};
|
||||
|
||||
writable_wrapper at(const key_type& bytes, name payer=current_context_contract()) {
|
||||
auto v = get(bytes, packed_tag{});
|
||||
check(std::get<0>(v), "key not found");
|
||||
return {std::get<1>(v), *std::get<0>(v), payer, owner};
|
||||
}
|
||||
|
||||
std::tuple<value_t*, key_type> get(key_type k, packed_tag) {
|
||||
using namespace internal_use_do_not_use;
|
||||
uint32_t sz;
|
||||
if (!kv_get(owner.value, k.data(), k.size(), sz))
|
||||
return {nullptr, std::move(k)};
|
||||
|
||||
auto val_bytes = get_tmp_buffer(sz);
|
||||
check(kv_get_data(0, val_bytes.data(), val_bytes.size()) == val_bytes.size(), "kv get internal failure");
|
||||
temp = unpack<value_t>(val_bytes.data(), val_bytes.size());
|
||||
return {&temp, std::move(k)};
|
||||
}
|
||||
|
||||
std::tuple<value_t*, key_type> get(const key_t& k) {
|
||||
return get(full_key(k), packed_tag{});
|
||||
}
|
||||
|
||||
inline bool set(const key_type& k, const value_t& v, name payer, packed_tag) const {
|
||||
using namespace internal_use_do_not_use;
|
||||
const auto& packed_value = pack_value(v);
|
||||
auto wrote = kv_set(owner.value, k.data(), k.size(), packed_value.data(), packed_value.size(), payer.value);
|
||||
return wrote == packed_value.size();
|
||||
}
|
||||
|
||||
inline bool set(const key_t& k, const value_t& v, name payer) const {
|
||||
return set(full_key(k), v, payer, packed_tag{});
|
||||
}
|
||||
|
||||
static detail::packed_view get_tmp_buffer(std::size_t size_needed=0) {
|
||||
constexpr std::size_t max_size = 512;
|
||||
static char static_data[max_size];
|
||||
static std::vector<char> dynamic_data = {0};
|
||||
if (size_needed > max_size) {
|
||||
if (dynamic_data.size() < size_needed)
|
||||
dynamic_data.resize(size_needed);
|
||||
return {dynamic_data.data(), size_needed};
|
||||
} else {
|
||||
return {&static_data[0], size_needed};
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Value>
|
||||
inline detail::packed_view pack_value(Value&& v) const {
|
||||
auto pv = get_tmp_buffer(pack_size(v));
|
||||
datastream<char*> ds(pv.data(), pv.size());
|
||||
ds << std::forward<Value>(v);
|
||||
return pv;
|
||||
}
|
||||
|
||||
private:
|
||||
name owner = current_context_contract();
|
||||
value_t temp; // used for
|
||||
};
|
||||
|
||||
} // namespace eosio::kv
|
||||
@@ -0,0 +1,551 @@
|
||||
#pragma once
|
||||
#include "../../core/eosio/context.hpp"
|
||||
#include "../../core/eosio/datastream.hpp"
|
||||
#include "../../core/eosio/name.hpp"
|
||||
#include "../../core/eosio/varint.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <iterator>
|
||||
|
||||
namespace eosio {
|
||||
|
||||
namespace detail {
|
||||
|
||||
template <typename Map>
|
||||
struct elem {
|
||||
using key_t = typename Map::key_t;
|
||||
using value_t = typename Map::value_t;
|
||||
using hash_key_t = typename Map::hash_key_t;
|
||||
using hash_t = typename Map::hash_t;
|
||||
using raw_iterator_t = typename Map::raw_iterator_t;
|
||||
using record_t = typename Map::record_t;
|
||||
|
||||
constexpr static inline raw_iterator_t invalidated_iterator = std::numeric_limits<raw_iterator_t>::max();
|
||||
|
||||
elem() = default;
|
||||
elem(const elem&) = default;
|
||||
elem(elem&& e) = default;
|
||||
elem& operator=(elem&&) = default;
|
||||
elem(key_t k)
|
||||
: hashed_key(hash_t{}(k)), key(std::move(k)) {}
|
||||
elem(key_t k, value_t v)
|
||||
: hashed_key(hash_t{}(k)), key(std::move(k)), value(std::move(v)) {}
|
||||
elem(const hash_key_t hk, const key_t& k, value_t v)
|
||||
: key(std::move(k)), hashed_key(hk), value(std::move(v)) {}
|
||||
elem(raw_iterator_t it, const hash_key_t hk, const key_t& k, value_t v)
|
||||
: raw_itr(it), key(std::move(k)), hashed_key(hk), value(std::move(v)) {}
|
||||
|
||||
void update(record_t&& r) {
|
||||
key = std::move(r.first);
|
||||
value = std::move(r.second);
|
||||
}
|
||||
|
||||
inline bool has_iterator() const {
|
||||
return raw_itr != invalidated_iterator;
|
||||
}
|
||||
|
||||
key_t key;
|
||||
hash_key_t hashed_key;
|
||||
raw_iterator_t raw_itr = invalidated_iterator;
|
||||
value_t value;
|
||||
};
|
||||
|
||||
template <class Alloc>
|
||||
struct buffer {
|
||||
buffer(size_t n)
|
||||
: size(n) {
|
||||
data = alloc.allocate(size);
|
||||
}
|
||||
buffer(buffer&& buf)
|
||||
: data(buf.data), size(buf.size) {
|
||||
}
|
||||
~buffer() {
|
||||
alloc.deallocate(data, size);
|
||||
}
|
||||
Alloc alloc;
|
||||
char* data;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct temp_alloc {
|
||||
|
||||
constexpr static std::size_t max_size = 512 / sizeof(T);
|
||||
static T static_data[max_size];
|
||||
static std::vector<T> dynamic_data;
|
||||
|
||||
// maintain std::allocator signature
|
||||
T* allocate(size_t size_needed) {
|
||||
if (size_needed > max_size) {
|
||||
if (dynamic_data.size() < size_needed)
|
||||
dynamic_data.resize(size_needed);
|
||||
return dynamic_data.data();
|
||||
} else {
|
||||
return &static_data[0];
|
||||
}
|
||||
}
|
||||
inline void deallocate(T*, size_t){}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
T temp_alloc<T>::static_data[temp_alloc<T>::max_size];
|
||||
template<typename T>
|
||||
std::vector<T> temp_alloc<T>::dynamic_data;
|
||||
|
||||
template <class Base>
|
||||
struct quadric_probing_policy {
|
||||
using key_t = typename Base::key_t;
|
||||
using hash_key_t = typename Base::hash_key_t;
|
||||
using raw_iterator_t = typename Base::raw_iterator_t;
|
||||
using record_t = typename Base::record_t;
|
||||
using bucket_t = record_t;
|
||||
using buffer_t = typename Base::buffer_t;
|
||||
using elem_t = typename Base::elem_t;
|
||||
|
||||
static constexpr inline raw_iterator_t invalidated_iterator = Base::elem_t::invalidated_iterator;
|
||||
|
||||
static inline bucket_t element_to_bucket(const elem_t& el) {
|
||||
return {el.key, el.value};
|
||||
}
|
||||
static inline record_t get_record(const bucket_t& b) {
|
||||
return b;
|
||||
}
|
||||
static inline hash_key_t next_hash(uint16_t collision_num, hash_key_t hash_key) {
|
||||
if (!collision_num)
|
||||
return hash_key;
|
||||
|
||||
auto inc = collision_num * collision_num;
|
||||
if (hash_key > std::numeric_limits<hash_key_t>::max() - inc) {
|
||||
return std::numeric_limits<hash_key_t>::max() - hash_key;
|
||||
};
|
||||
|
||||
return hash_key + inc;
|
||||
}
|
||||
inline bucket_t extract_bucket(hash_key_t hashed_key, raw_iterator_t& raw_itr, const key_t& k) const {
|
||||
return extract_bucket(hashed_key, raw_itr);
|
||||
}
|
||||
bucket_t extract_bucket(hash_key_t hashed_key, raw_iterator_t& raw_itr) const {
|
||||
using namespace internal_use_do_not_use;
|
||||
|
||||
raw_itr = db_find_i64( owner.value, scope.value, Base::table_name.value, hashed_key );
|
||||
if( raw_itr >= 0 ) {
|
||||
return extract_bucket(raw_itr);
|
||||
}
|
||||
|
||||
raw_itr = invalidated_iterator;
|
||||
return {};
|
||||
}
|
||||
bucket_t extract_bucket(raw_iterator_t itr) const {
|
||||
using namespace internal_use_do_not_use;
|
||||
|
||||
check(itr != invalidated_iterator, "invalid iterator passed");
|
||||
|
||||
uint32_t size = db_get_i64( itr, nullptr, 0 );
|
||||
buffer_t buff(size);
|
||||
|
||||
auto wrote = db_get_i64( itr, buff.data, buff.size );
|
||||
check(wrote == buff.size, "kv get internal failure");
|
||||
|
||||
return unpack<bucket_t>(buff.data, buff.size);
|
||||
}
|
||||
|
||||
name owner;
|
||||
name scope;
|
||||
};
|
||||
|
||||
template <class Map>
|
||||
struct iterator {
|
||||
using elem_t = typename Map::elem_t;
|
||||
using record_t = typename Map::record_t;
|
||||
using bucket_t = typename Map::bucket_t;
|
||||
using raw_iterator_t = typename Map::raw_iterator_t;
|
||||
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
using value_type = typename elem_t::value_t;
|
||||
using difference_type = size_t;
|
||||
using pointer = record_t*;
|
||||
using reference = record_t&;
|
||||
|
||||
using map_t = Map;
|
||||
|
||||
static constexpr inline raw_iterator_t invalidated_iterator = elem_t::invalidated_iterator;
|
||||
|
||||
inline iterator() : element(invalidated_iterator, {}, {}, {}) {}
|
||||
inline iterator(raw_iterator_t itr, const map_t* m)
|
||||
: element(itr, {}, {}, {}), map(m) {}
|
||||
inline iterator(elem_t&& el, const map_t* m) : element(std::move(el)), map(m) {}
|
||||
|
||||
iterator(const iterator&) = default;
|
||||
iterator& operator=(const iterator&) = default;
|
||||
|
||||
inline iterator(iterator&& o)
|
||||
: element(o.element) {
|
||||
}
|
||||
|
||||
inline iterator& operator=(iterator&& o) {
|
||||
return const_cast<iterator&>(const_cast<const iterator&>(*this).operator=(std::forward<iterator&&>(o)));
|
||||
}
|
||||
inline const iterator& operator=(iterator&& o) const {
|
||||
element = std::move(o.element);
|
||||
map = o.map;
|
||||
return *this;
|
||||
}
|
||||
|
||||
~iterator() {}
|
||||
|
||||
inline elem_t& operator*() {
|
||||
materialize();
|
||||
return element;
|
||||
}
|
||||
inline const elem_t& operator*() const {
|
||||
materialize();
|
||||
return element;
|
||||
}
|
||||
inline elem_t* operator->() {
|
||||
materialize();
|
||||
return &element;
|
||||
}
|
||||
inline const elem_t* operator->() const {
|
||||
materialize();
|
||||
return &element;
|
||||
}
|
||||
|
||||
iterator& operator++() const {
|
||||
assert_valid();
|
||||
|
||||
uint64_t next_pk;
|
||||
auto next_itr = internal_use_do_not_use::db_next_i64( element.raw_itr, &next_pk );
|
||||
if( next_itr < 0 ) {
|
||||
element.raw_itr = invalidated_iterator;
|
||||
}
|
||||
else {
|
||||
element.raw_itr = next_itr;
|
||||
}
|
||||
return const_cast<iterator&>(*this);
|
||||
}
|
||||
|
||||
inline iterator& operator++(int) const {
|
||||
auto temp = *this;
|
||||
++(*this);
|
||||
return const_cast<iterator&>(temp);
|
||||
}
|
||||
|
||||
inline bool operator==(const iterator& o) const {
|
||||
return (element.raw_itr == o.element.raw_itr);
|
||||
}
|
||||
|
||||
inline bool operator!=(const iterator& o) const { return !((*this) == o); }
|
||||
|
||||
void materialize() const {
|
||||
assert_valid();
|
||||
static bucket_t bucket_data;
|
||||
bucket_data = map->collision_policy.extract_bucket(element.raw_itr);
|
||||
element.update( map->collision_policy.get_record(bucket_data) );
|
||||
}
|
||||
|
||||
// mutable for const_iterator. we do not consider increment and element change as non const operation
|
||||
// the only methods where constness play role are dereference and pointer
|
||||
mutable elem_t element;
|
||||
mutable const map_t* map;
|
||||
|
||||
private:
|
||||
inline void assert_valid() const {
|
||||
eosio::check( element.raw_itr != invalidated_iterator, "cannot increment end iterator" );
|
||||
}
|
||||
};
|
||||
|
||||
template<class Map>
|
||||
using const_iterator = const iterator<Map>;
|
||||
} // namespace eosio::detail
|
||||
|
||||
template <eosio::name::raw TableName,
|
||||
typename Key,
|
||||
typename Value,
|
||||
class Hash = std::hash<Key>,
|
||||
template <typename> class CollisionPolicy = detail::quadric_probing_policy,
|
||||
template <typename> class Allocator = detail::temp_alloc>
|
||||
class unordered_map {
|
||||
public:
|
||||
constexpr static inline name table_name = name{static_cast<uint64_t>(TableName)};
|
||||
static_assert( table_name && table_name.length() < 13, "multi_index does not support table names with a length greater than 12");
|
||||
constexpr static uint16_t max_collisions = 255;
|
||||
|
||||
using key_t = Key;
|
||||
using value_t = Value;
|
||||
using record_t = std::pair<Key, Value>;
|
||||
using hash_key_t = uint64_t;
|
||||
using hash_t = Hash;
|
||||
using raw_iterator_t = int32_t;
|
||||
using allocator_t = Allocator<char>;
|
||||
using buffer_t = detail::buffer<allocator_t>;
|
||||
using self_t = unordered_map<TableName, Key, Value, Hash, CollisionPolicy, Allocator>;
|
||||
using elem_t = detail::elem<self_t>;
|
||||
using iterator_t = detail::iterator<self_t>;
|
||||
using const_iterator_t = detail::const_iterator<self_t>;
|
||||
|
||||
using collision_policy_t = CollisionPolicy<self_t>;
|
||||
using bucket_t = typename collision_policy_t::record_t;
|
||||
|
||||
|
||||
struct writable_wrapper {
|
||||
using unordered_map_t = self_t;
|
||||
writable_wrapper(key_t k, value_t v, name p, unordered_map_t& m)
|
||||
: element(std::move(k), std::move(v)), payer(p), map_ref(m) {}
|
||||
writable_wrapper(elem_t el, name p, unordered_map_t& m)
|
||||
: element(std::move(el)), payer(p), map_ref(m) {}
|
||||
|
||||
explicit operator value_t&() { return element.value; }
|
||||
operator value_t() const { return element.value; }
|
||||
|
||||
writable_wrapper& operator=(const value_t& o) {
|
||||
element.value = o;
|
||||
map_ref.set(element, payer);
|
||||
return *this;
|
||||
}
|
||||
|
||||
writable_wrapper& operator=(value_t&& o) {
|
||||
element.value = std::move(o);
|
||||
map_ref.set(element, payer);
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool operator == (const writable_wrapper& w) {
|
||||
return std::tie(element.key, element.value) == std::tie(w.element.key, w.element.value);
|
||||
}
|
||||
bool operator == (const value_t& v) {
|
||||
return element.value == v;
|
||||
}
|
||||
|
||||
elem_t element;
|
||||
name payer;
|
||||
unordered_map_t& map_ref;
|
||||
};
|
||||
|
||||
inline unordered_map(name o, name s)
|
||||
: owner(o), scope(s), collision_policy{o, s} {}
|
||||
|
||||
/**
|
||||
* Basic constructor of N elements.
|
||||
*/
|
||||
inline unordered_map(name o, name s, std::initializer_list<elem_t> l)
|
||||
: owner(o), scope(s), collision_policy{o, s} {
|
||||
for ( auto e : l ) {
|
||||
set(e, owner);
|
||||
}
|
||||
}
|
||||
|
||||
writable_wrapper operator[](const std::pair<key_t, name>& key_payer) {
|
||||
elem_t el{key_payer.first};
|
||||
if (!get(el))
|
||||
set(el, key_payer.second);
|
||||
|
||||
return {el, key_payer.second, *this};
|
||||
}
|
||||
|
||||
writable_wrapper operator[](const key_t& k) {
|
||||
return (*this)[{k, owner}];
|
||||
}
|
||||
writable_wrapper operator[](key_t&& k) {
|
||||
return (*this)[{std::move(k), owner}];
|
||||
}
|
||||
|
||||
writable_wrapper at(key_t&& k, name payer) {
|
||||
elem_t el{std::forward<key_t>(k)};
|
||||
check(get(el), "key not found");
|
||||
return {el, payer, *this};
|
||||
}
|
||||
writable_wrapper at(key_t&& k) {
|
||||
return at(std::forward<key_t&&>(k), owner);
|
||||
}
|
||||
writable_wrapper at(const key_t& k, name payer) {
|
||||
elem_t el{k};
|
||||
check(get(el), "key not found");
|
||||
return {el, payer, *this};
|
||||
}
|
||||
writable_wrapper at(const key_t& k) {
|
||||
return at(k, owner);
|
||||
}
|
||||
|
||||
inline iterator_t begin() const {
|
||||
return lower_bound_internal(std::numeric_limits<hash_key_t>::lowest());
|
||||
}
|
||||
inline const_iterator_t cbegin() const {
|
||||
return begin();
|
||||
}
|
||||
|
||||
inline iterator_t end() const {
|
||||
return {};
|
||||
}
|
||||
inline const_iterator_t cend() const {
|
||||
return end();
|
||||
}
|
||||
|
||||
inline bool empty() const {
|
||||
return cbegin() == cend();
|
||||
}
|
||||
|
||||
inline size_t size() const {
|
||||
return std::distance(begin(), end());
|
||||
}
|
||||
|
||||
inline void clear() {
|
||||
erase(begin(), end());
|
||||
}
|
||||
|
||||
inline std::pair<iterator_t,bool> insert( record_t&& rec ) {
|
||||
elem_t el{rec.key, rec.value};
|
||||
bool updated = get(el);
|
||||
set(el);
|
||||
return {{el, this}, !updated};
|
||||
}
|
||||
|
||||
template< class InputIt >
|
||||
void insert( InputIt begin, InputIt end ) {
|
||||
while (begin != end) {
|
||||
elem_t el{begin.first, end.second};
|
||||
get(el);// we need this in case of collisions
|
||||
set(el);
|
||||
|
||||
++begin;
|
||||
}
|
||||
}
|
||||
inline void insert( std::initializer_list<record_t> ilist ) {
|
||||
insert( ilist.begin(), ilist.end() );
|
||||
}
|
||||
|
||||
iterator_t find(const key_t& k) const {
|
||||
elem_t el{k, owner};
|
||||
return get(el) ? iterator_t{el, this} : end();
|
||||
}
|
||||
|
||||
void erase(const key_t& k) {
|
||||
using namespace internal_use_do_not_use;
|
||||
elem_t el{k};
|
||||
check(get(el), "no such key exist");
|
||||
db_remove_i64(el.raw_itr);
|
||||
}
|
||||
|
||||
iterator_t erase(const const_iterator_t& it) {
|
||||
using namespace internal_use_do_not_use;
|
||||
|
||||
auto raw_itr = (*it).raw_itr;
|
||||
++it;
|
||||
db_remove_i64(raw_itr);
|
||||
return const_cast<iterator_t&>(it);
|
||||
}
|
||||
inline iterator_t erase(iterator_t& it) {
|
||||
return erase(it);
|
||||
}
|
||||
iterator_t erase(const const_iterator_t& begin, const const_iterator_t& end) {
|
||||
while (begin != end) {
|
||||
begin = erase(begin);
|
||||
}
|
||||
return const_cast<iterator_t&>(end);
|
||||
}
|
||||
inline iterator_t erase(iterator_t& begin, iterator_t& end) {
|
||||
return erase(begin, end);
|
||||
}
|
||||
|
||||
inline iterator_t lower_bound(const key_t& k) const {
|
||||
return lower_bound_internal(hash_t{}(k));
|
||||
}
|
||||
protected:
|
||||
// no need to expose method that takes raw hash
|
||||
iterator_t lower_bound_internal(const hash_key_t& hk) const {
|
||||
using namespace internal_use_do_not_use;
|
||||
|
||||
auto itr = db_lowerbound_i64( owner.value, scope.value, table_name.value, hk );
|
||||
if( itr < 0 ) return end();
|
||||
|
||||
return {{itr, {}, {}, {}}, this};
|
||||
}
|
||||
public:
|
||||
iterator_t upper_bound(const key_t& k) const {
|
||||
//we need manually increment hash
|
||||
auto hashed_key = hash_t{}(k);
|
||||
auto next_hash = collision_policy.next_hash(1, hashed_key);
|
||||
if (hashed_key > next_hash)
|
||||
return end();
|
||||
|
||||
return lower_bound_internal( next_hash );
|
||||
}
|
||||
|
||||
inline std::pair<iterator_t, iterator_t> equal_range(const key_t& k) {
|
||||
return {lower_bound(k), upper_bound(k)};
|
||||
}
|
||||
inline std::pair<const_iterator_t, const_iterator_t> equal_range(const key_t& k) const {
|
||||
return equal_range(k);
|
||||
}
|
||||
|
||||
inline bool contains(const key_t& k) const {
|
||||
elem_t el{k};
|
||||
return get(el);
|
||||
}
|
||||
|
||||
friend iterator_t;
|
||||
|
||||
protected:
|
||||
|
||||
bool get(elem_t& element) const {
|
||||
|
||||
uint16_t cur_collision = 0;
|
||||
static bucket_t temp_bucket;
|
||||
static record_t temp_record;
|
||||
do {
|
||||
//intential post increment to have zero for first call. in that case next_hash supposed to return the key unchanged
|
||||
element.hashed_key = collision_policy.next_hash(cur_collision++, element.hashed_key);
|
||||
temp_bucket = collision_policy.extract_bucket(element.hashed_key, element.raw_itr, element.key);
|
||||
temp_record = collision_policy.get_record(temp_bucket);
|
||||
// loop if collision
|
||||
} while ( temp_record != record_t{} &&
|
||||
temp_record.first != element.key &&
|
||||
cur_collision < max_collisions );
|
||||
|
||||
bool found = temp_record != record_t{};
|
||||
if (found)
|
||||
element.value = std::move(temp_record.second);
|
||||
return found;
|
||||
}
|
||||
|
||||
inline void set(elem_t& el, name payer) const {
|
||||
using namespace internal_use_do_not_use;
|
||||
|
||||
auto temp_el = el;
|
||||
static bucket_t temp_bucket;
|
||||
temp_bucket = collision_policy.element_to_bucket(el);
|
||||
if (!el.has_iterator() && !get(el)) {
|
||||
buffer_t buffer = pack_value(temp_bucket);
|
||||
el.raw_itr = db_store_i64( scope.value, table_name.value, payer.value, el.hashed_key, buffer.data, buffer.size );
|
||||
check(el.has_iterator(), "error inserting element");
|
||||
}
|
||||
else {
|
||||
check(el.has_iterator(), "element can't be updated as it was not found");
|
||||
buffer_t buffer = pack_value(temp_bucket);
|
||||
db_update_i64( el.raw_itr, payer.value, buffer.data, buffer.size );
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline buffer_t pack_value(T&& v) const {
|
||||
buffer_t buffer(pack_size(v));
|
||||
datastream<char*> ds(buffer.data, buffer.size);
|
||||
ds << std::forward<T>(v);
|
||||
return std::move(buffer);
|
||||
}
|
||||
|
||||
private:
|
||||
name owner;
|
||||
name scope;
|
||||
allocator_t alloc;
|
||||
collision_policy_t collision_policy;
|
||||
|
||||
friend detail::iterator<self_t>;
|
||||
};
|
||||
|
||||
} // namespace eosio
|
||||
@@ -1,343 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
|
||||
#include "datastream.hpp"
|
||||
#include "reflect.hpp"
|
||||
|
||||
namespace eosio {
|
||||
|
||||
namespace detail {
|
||||
template<typename T>
|
||||
constexpr bool has_bitwise_serialization() {
|
||||
if constexpr (std::is_arithmetic_v<T>) {
|
||||
return true;
|
||||
} else if constexpr (std::is_enum_v<T>) {
|
||||
static_assert(!std::is_convertible_v<T, std::underlying_type_t<T>>, "Serializing unscoped enum");
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
template <template <typename> class C, typename T>
|
||||
constexpr bool is_ranged_type(C<T>) {
|
||||
using type = std::decay_t<C<T>>;
|
||||
return
|
||||
std::is_same_v<std::vector<T>, type> ||
|
||||
std::is_same_v<std::list<T>, type> ||
|
||||
std::is_same_v<std::deque<T>, type> ||
|
||||
std::is_same_v<std::set<T>, type>;
|
||||
}
|
||||
|
||||
template <typename R, typename C>
|
||||
auto member_pointer_type(R (C::*)) -> R;
|
||||
template <typename R, typename C>
|
||||
auto member_pointer_class(R (C::*)) -> C;
|
||||
|
||||
template <typename... Args>
|
||||
constexpr inline std::size_t total_bytes_size() { return (sizeof(Args) + ...); }
|
||||
|
||||
// TODO rework the to_key and datastream logic to be constexpr/consteval friendly to get rid of this
|
||||
template <std::size_t I, typename Arg, typename... Args>
|
||||
inline void const_pack_helper(std::string& s, Arg&& arg, Args&&... args) {
|
||||
std::memcpy(s.data()+I, &arg, sizeof(Arg));
|
||||
if constexpr (std::is_integral_v<std::decay_t<Arg>> ||
|
||||
std::is_same_v<std::decay_t<Arg>, eosio::name>) {
|
||||
std::reverse(s.data()+I, s.data()+I+sizeof(Arg));
|
||||
}
|
||||
if constexpr (sizeof...(Args) > 0) {
|
||||
return const_pack_helper<I+sizeof(Arg)>(s, std::forward<Args>(args)...);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO rework the to_key and datastream logic to be constexpr/consteval friendly to get rid of this
|
||||
template <typename... Args>
|
||||
inline std::string const_pack(Args&&... args) {
|
||||
std::string s;
|
||||
s.resize(total_bytes_size<Args...>());
|
||||
const_pack_helper<0>(s, std::forward<Args>(args)...);
|
||||
return s;
|
||||
}
|
||||
} // namespace eosio::detail
|
||||
|
||||
using key_type = std::string;
|
||||
|
||||
// to_key defines a conversion from a type to a sequence of bytes whose lexicograpical
|
||||
// ordering is the same as the ordering of the original type.
|
||||
//
|
||||
// For any two objects of type T, a and b:
|
||||
//
|
||||
// - key(a) < key(b) iff a < b
|
||||
// - key(a) is not a prefix of key(b)
|
||||
//
|
||||
// Overloads of to_key for user-defined types can be found by Koenig (ADL) lookup.
|
||||
//
|
||||
// to_key is specialized for the following types
|
||||
// - std::string and std::string_view
|
||||
// - std::vector, std::list, std::deque
|
||||
// - std::tuple
|
||||
// - std::array
|
||||
// - std::optional
|
||||
// - std::variant
|
||||
// - Arithmetic types
|
||||
// - Scoped enumeration types
|
||||
// - Reflected structs
|
||||
// - All smart-contract related types defined by abieos
|
||||
template <typename T, typename S>
|
||||
void to_key(const T& obj, datastream<S>& stream);
|
||||
|
||||
template <int I, typename T, typename S>
|
||||
void to_key_tuple(const T& obj, datastream<S>& stream) {
|
||||
if constexpr (I < std::tuple_size_v<T>) {
|
||||
to_key(std::get<I>(obj), stream);
|
||||
to_key_tuple<I + 1>(obj, stream);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename... Ts, typename S>
|
||||
void to_key(const std::tuple<Ts...>& obj, datastream<S>& stream) {
|
||||
to_key_tuple<0>(obj, stream);
|
||||
}
|
||||
|
||||
template <typename T, std::size_t N, typename S>
|
||||
void to_key(const std::array<T, N>& obj, datastream<S>& stream) {
|
||||
for (const T& elem : obj) { to_key(elem, stream); }
|
||||
}
|
||||
|
||||
template <typename T, typename S>
|
||||
void to_key_optional(const bool* obj, datastream<S>& stream) {
|
||||
if (obj == nullptr)
|
||||
stream.write('\0');
|
||||
else if (!*obj)
|
||||
stream.write('\1');
|
||||
else {
|
||||
stream.write('\2');
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, typename S>
|
||||
void to_key_optional(const T* obj, datastream<S>& stream) {
|
||||
if constexpr (detail::has_bitwise_serialization<T>() && sizeof(T) == 1) {
|
||||
if (obj == nullptr)
|
||||
stream.write("\0", 2);
|
||||
else {
|
||||
char buf[1];
|
||||
datastream<char*> tmp_stream(buf, 1);
|
||||
to_key(*obj, tmp_stream);
|
||||
stream.write(buf[0]);
|
||||
if (buf[0] == '\0')
|
||||
stream.write('\1');
|
||||
}
|
||||
} else {
|
||||
if (obj) {
|
||||
stream.write('\1');
|
||||
to_key(*obj, stream);
|
||||
} else {
|
||||
stream.write('\0');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, typename U, typename S>
|
||||
void to_key(const std::pair<T, U>& obj, datastream<S>& stream) {
|
||||
to_key(obj.first, stream);
|
||||
to_key(obj.second, stream);
|
||||
}
|
||||
|
||||
template <typename T, typename S>
|
||||
void to_key_range(const T& obj, datastream<S>& stream) {
|
||||
for (const auto& elem : obj) { to_key_optional(&elem, stream); }
|
||||
to_key_optional((std::add_pointer_t<decltype(*std::begin(obj))>) nullptr, stream);
|
||||
}
|
||||
|
||||
template <typename T, typename S>
|
||||
auto to_key(const T& obj, datastream<S>& stream) -> std::enable_if_t<is_ranged_type(std::declval<T>()), void> {
|
||||
to_key_range(obj, stream);
|
||||
}
|
||||
|
||||
template <typename T, typename U, typename S>
|
||||
void to_key(const std::map<T, U>& obj, datastream<S>& stream) {
|
||||
to_key_range(obj, stream);
|
||||
}
|
||||
|
||||
template <typename T, typename S>
|
||||
void to_key(const std::optional<T>& obj, datastream<S>& stream) {
|
||||
to_key_optional(obj ? &*obj : nullptr, stream);
|
||||
}
|
||||
|
||||
// The first byte holds:
|
||||
// 0-4 1's (number of additional bytes) 0 (terminator) bits
|
||||
//
|
||||
// The number is represented as big-endian using the low order
|
||||
// bits of the first byte and all of the remaining bytes.
|
||||
//
|
||||
// Notes:
|
||||
// - values must be encoded using the minimum number of bytes,
|
||||
// as non-canonical representations will break the sort order.
|
||||
template <typename S>
|
||||
void to_key_varuint32(std::uint32_t obj, datastream<S>& stream) {
|
||||
int num_bytes;
|
||||
if (obj < 0x80u) {
|
||||
num_bytes = 1;
|
||||
} else if (obj < 0x4000u) {
|
||||
num_bytes = 2;
|
||||
} else if (obj < 0x200000u) {
|
||||
num_bytes = 3;
|
||||
} else if (obj < 0x10000000u) {
|
||||
num_bytes = 4;
|
||||
} else {
|
||||
num_bytes = 5;
|
||||
}
|
||||
|
||||
stream.write(
|
||||
static_cast<char>(~(0xFFu >> (num_bytes - 1)) | (num_bytes == 5 ? 0 : (obj >> ((num_bytes - 1) * 8)))));
|
||||
for (int i = num_bytes - 2; i >= 0; --i) { stream.write(static_cast<char>((obj >> i * 8) & 0xFFu)); }
|
||||
}
|
||||
|
||||
// for non-negative values
|
||||
// The first byte holds:
|
||||
// 1 (signbit) 0-4 1's (number of additional bytes) 0 (terminator) bits
|
||||
// The value is represented as big endian
|
||||
// for negative values
|
||||
// The first byte holds:
|
||||
// 0 (signbit) 0-4 0's (number of additional bytes) 1 (terminator) bits
|
||||
// The value is adjusted to be positive based on the range that can
|
||||
// be represented with this number of bytes and then encoded as big endian.
|
||||
//
|
||||
// Notes:
|
||||
// - negative values must sort before positive values
|
||||
// - For negative value, numbers that need more bytes are smaller, hence
|
||||
// the encoding of the width must be opposite the encoding used for
|
||||
// non-negative values.
|
||||
// - A 5-byte varint can represent values in $[-2^34, 2^34)$. In this case,
|
||||
// the argument will be sign-extended.
|
||||
template <typename S>
|
||||
void to_key_varint32(std::int32_t obj, datastream<S>& stream) {
|
||||
static_assert(std::is_same_v<S, void>, "to_key for varint32 has been temporarily disabled");
|
||||
int num_bytes;
|
||||
bool sign = (obj < 0);
|
||||
if (obj < 0x40 && obj >= -0x40) {
|
||||
num_bytes = 1;
|
||||
} else if (obj < 0x2000 && obj >= -0x2000) {
|
||||
num_bytes = 2;
|
||||
} else if (obj < 0x100000 && obj >= -0x100000) {
|
||||
num_bytes = 3;
|
||||
} else if (obj < 0x08000000 && obj >= -0x08000000) {
|
||||
num_bytes = 4;
|
||||
} else {
|
||||
num_bytes = 5;
|
||||
}
|
||||
|
||||
unsigned char width_field;
|
||||
if (sign) {
|
||||
width_field = 0x80u >> num_bytes;
|
||||
} else {
|
||||
width_field = 0x80u | ~(0xFFu >> num_bytes);
|
||||
}
|
||||
auto uobj = static_cast<std::uint32_t>(obj);
|
||||
unsigned char value_mask = (0xFFu >> (num_bytes + 1));
|
||||
unsigned char high_byte = (num_bytes == 5 ? (sign ? 0xFF : 0) : (uobj >> ((num_bytes - 1) * 8)));
|
||||
stream.write(width_field | (high_byte & value_mask));
|
||||
for (int i = num_bytes - 2; i >= 0; --i) { stream.write(static_cast<char>((uobj >> i * 8) & 0xFFu)); }
|
||||
}
|
||||
|
||||
template <typename... Ts, typename S>
|
||||
void to_key(const std::variant<Ts...>& obj, datastream<S>& stream) {
|
||||
to_key_varuint32(static_cast<uint32_t>(obj.index()), stream);
|
||||
std::visit([&](const auto& item) { to_key(item, stream); }, obj);
|
||||
}
|
||||
|
||||
template <std::size_t N, typename S>
|
||||
void to_key(const char (&str)[N], datastream<S>& stream) {
|
||||
to_key(std::string_view{str, N-1}, stream);
|
||||
}
|
||||
|
||||
template <typename S>
|
||||
void to_key(std::string_view obj, datastream<S>& stream) {
|
||||
for (char ch : obj) {
|
||||
stream.write(ch);
|
||||
if (ch == '\0') {
|
||||
stream.write('\1');
|
||||
}
|
||||
}
|
||||
stream.write("\0", 2);
|
||||
}
|
||||
|
||||
template <typename S>
|
||||
void to_key(const std::string& obj, datastream<S>& stream) {
|
||||
to_key(std::string_view(obj), stream);
|
||||
}
|
||||
|
||||
template <typename S>
|
||||
void to_key(bool obj, datastream<S>& stream) {
|
||||
stream.write(static_cast<char>(obj ? 1 : 0));
|
||||
}
|
||||
|
||||
template <typename UInt, typename T>
|
||||
UInt float_to_key(T value) {
|
||||
static_assert(sizeof(T) == sizeof(UInt), "Expected unsigned int of the same size");
|
||||
UInt result;
|
||||
std::memcpy(&result, &value, sizeof(T));
|
||||
UInt signbit = (static_cast<UInt>(1) << (std::numeric_limits<UInt>::digits - 1));
|
||||
UInt mask = 0;
|
||||
if (result == signbit)
|
||||
result = 0;
|
||||
if (result & signbit)
|
||||
mask = ~mask;
|
||||
return result ^ (mask | signbit);
|
||||
}
|
||||
|
||||
template <typename T, typename S>
|
||||
void to_key(const T& obj, datastream<S>& stream) {
|
||||
if constexpr (std::is_floating_point_v<T>) {
|
||||
if constexpr (sizeof(T) == 4) {
|
||||
to_key(float_to_key<uint32_t>(obj), stream);
|
||||
} else {
|
||||
static_assert(sizeof(T) == 8, "Unknown floating point type");
|
||||
to_key(float_to_key<uint64_t>(obj), stream);
|
||||
}
|
||||
} else if constexpr (std::is_integral_v<T>) {
|
||||
auto v = static_cast<std::make_unsigned_t<T>>(obj);
|
||||
v -= static_cast<std::make_unsigned_t<T>>(std::numeric_limits<T>::min());
|
||||
std::reverse(reinterpret_cast<char*>(&v), reinterpret_cast<char*>(&v + 1));
|
||||
stream.write(&v, sizeof(v));
|
||||
} else if constexpr (std::is_enum_v<T>) {
|
||||
static_assert(!std::is_convertible_v<T, std::underlying_type_t<T>>, "Serializing unscoped enum");
|
||||
to_key(static_cast<std::underlying_type_t<T>>(obj), stream);
|
||||
} else {
|
||||
using ts_meta = bluegrass::meta::meta_object<T>;
|
||||
ts_meta::for_each_field(obj, [&](const auto& member) {
|
||||
to_key(member, stream);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void convert_to_key(const T& t, key_type& bin) {
|
||||
datastream<size_t> ss;
|
||||
to_key(t, ss);
|
||||
auto orig_size = bin.size();
|
||||
bin.resize(orig_size + ss.tellp());
|
||||
datastream<char*> fbs(bin.data() + orig_size, ss.tellp());
|
||||
to_key(t, fbs);
|
||||
check( fbs.valid(), "Stream overrun" );
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
key_type convert_to_key(const T& t) {
|
||||
key_type result;
|
||||
convert_to_key(t, result);
|
||||
return result;
|
||||
}
|
||||
} // namespace eosio
|
||||
@@ -22,5 +22,8 @@ namespace eosio::testing {
|
||||
|
||||
static std::vector<uint8_t> crypto_primitives_test_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/crypto_primitives_tests.wasm"); }
|
||||
static std::vector<char> crypto_primitives_test_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/crypto_primitives_tests.abi"); }
|
||||
|
||||
static std::vector<uint8_t> kv_map_tests_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/kv_map_tests.wasm"); }
|
||||
static std::vector<char> kv_map_tests_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/kv_map_tests.abi"); }
|
||||
};
|
||||
} //ns eosio::testing
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#include <boost/test/data/test_case.hpp>
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <eosio/chain/abi_serializer.hpp>
|
||||
#include <eosio/testing/tester.hpp>
|
||||
|
||||
//#include <fc/variant_object.hpp>
|
||||
|
||||
#include <contracts.hpp>
|
||||
|
||||
using namespace eosio;
|
||||
using namespace eosio::testing;
|
||||
|
||||
struct kv_tester {
|
||||
kv_tester(std::vector<uint8_t> wasm, std::vector<char> abi) {
|
||||
|
||||
chain.create_accounts({"kvtest"_n});
|
||||
chain.produce_block();
|
||||
chain.set_code("kvtest"_n, wasm);
|
||||
chain.set_abi("kvtest"_n, abi.data());
|
||||
chain.produce_blocks();
|
||||
}
|
||||
|
||||
void push_action(name act, std::string exception_msg="") {
|
||||
if (exception_msg.empty()) {
|
||||
chain.push_action("kvtest"_n, act, "kvtest"_n, {});
|
||||
} else {
|
||||
BOOST_CHECK_EXCEPTION(chain.push_action("kvtest"_n, act, "kvtest"_n, {}),
|
||||
eosio_assert_message_exception,
|
||||
eosio_assert_message_is(exception_msg));
|
||||
}
|
||||
}
|
||||
|
||||
tester chain;
|
||||
};
|
||||
|
||||
BOOST_AUTO_TEST_SUITE(key_value_tests)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(map_tests) try {
|
||||
kv_tester t = {contracts::kv_map_tests_wasm(), contracts::kv_map_tests_abi()};
|
||||
t.push_action("test"_n);
|
||||
t.push_action("iter"_n);
|
||||
t.push_action("erase"_n);
|
||||
t.push_action("eraseexcp"_n, "key not found");
|
||||
t.push_action("bounds"_n);
|
||||
t.push_action("ranges"_n);
|
||||
t.push_action("empty"_n);
|
||||
} FC_LOG_AND_RETHROW()
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
@@ -7,6 +7,7 @@ add_contract(explicit_nested_tests explicit_nested_tests explicit_nested_tests.c
|
||||
add_contract(transfer_contract transfer_contract transfer.cpp)
|
||||
add_contract(minimal_tests minimal_tests minimal_tests.cpp)
|
||||
add_contract(crypto_primitives_tests crypto_primitives_tests crypto_primitives_tests.cpp)
|
||||
add_contract(kv_map_tests kv_map_tests kv_map_tests.cpp)
|
||||
add_contract(capi_tests capi_tests capi/capi.c capi/action.c capi/chain.c capi/crypto.c capi/db.c capi/permission.c
|
||||
capi/print.c capi/privileged.c capi/system.c capi/transaction.c)
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
#include <eosio/eosio.hpp>
|
||||
#include <eosio/unordered_map.hpp>
|
||||
|
||||
struct test_record {
|
||||
int pk;
|
||||
float s;
|
||||
std::string n;
|
||||
};
|
||||
|
||||
class [[eosio::contract]] kv_map_tests : public eosio::contract {
|
||||
public:
|
||||
using contract::contract;
|
||||
|
||||
using testmap_t = eosio::unordered_map<"testmap"_n, int, float>;
|
||||
using testmap2_t = eosio::unordered_map<"testmap2"_n, std::string, std::string>;
|
||||
using testmap3_t = eosio::unordered_map<"testmap3"_n, int, float>;
|
||||
|
||||
[[eosio::action]]
|
||||
void test() {
|
||||
testmap_t t = { "kvtest"_n, "kvtest"_n, {{33, 23.4f}, {10, 13.44f}, {103, 334.3f}} };
|
||||
|
||||
auto p = t[33];
|
||||
|
||||
p = 102.23; // note here this will update the held value and do a db set
|
||||
|
||||
testmap_t t2{"kvtest"_n, "kvtest"_n};
|
||||
|
||||
eosio::check(p == 102.23f, "should be the same value");
|
||||
eosio::check(p == t.at(33), "should be the same value");
|
||||
|
||||
auto it = t.begin();
|
||||
|
||||
auto& el = *it;
|
||||
|
||||
eosio::check(el.value == 13.44f, "should still be the same from before");
|
||||
|
||||
testmap2_t t3 = { "kvtest"_n, "kvtest"_n, {{"eosio", "fast"}, {"bit...", "hmm"}} };
|
||||
|
||||
auto it2 = t3.begin();
|
||||
auto& el2 = *it2;
|
||||
|
||||
++it2;
|
||||
++it2;
|
||||
auto it3 = std::move(it2);
|
||||
|
||||
it2 = t3.end();
|
||||
|
||||
eosio::check(it2 == it3, "they should be at the end and pointing to the same thing");
|
||||
eosio::check(it2 == t3.end(), "iterator should be at end");
|
||||
eosio::check(it3 == t3.end(), "iterator should be at end");
|
||||
}
|
||||
|
||||
[[eosio::action]]
|
||||
void iter() {
|
||||
testmap_t t = {"kvtest"_n, "kvtest"_n, {{34, 23.4f}, {11, 13.44f}, {104, 334.3f}, {5, 33.42f}} };
|
||||
|
||||
//called after test() and contains more data
|
||||
std::map<int, float> test_vals = {{34, 23.4f}, {11, 13.44f}, {104, 334.3f}, {5, 33.42f}, {33, 102.23f}, {10, 13.44f}, {103, 334.3f}};
|
||||
|
||||
int i = 0;
|
||||
|
||||
// test that this will work with auto ranged for loop
|
||||
for ( const auto& e : t ) {
|
||||
++i;
|
||||
eosio::check(test_vals.find(e.key)->second == e.value, "invalid value in iter test");
|
||||
}
|
||||
eosio::check(t.size() == 7, "size is wrong");
|
||||
eosio::check(i == 7, "range based loop iterates less elements than there are in container");
|
||||
}
|
||||
|
||||
[[eosio::action]]
|
||||
void erase() {
|
||||
testmap_t t("kvtest"_n, "kvtest"_n);
|
||||
|
||||
t.contains(34);
|
||||
t.erase(34);
|
||||
|
||||
eosio::check(!t.contains(34), "should have erased a value");
|
||||
}
|
||||
|
||||
[[eosio::action]]
|
||||
void eraseexcp() {
|
||||
testmap_t t("kvtest"_n, "kvtest"_n);
|
||||
t.at(34); // this should cause an assertion
|
||||
}
|
||||
|
||||
[[eosio::action]]
|
||||
void bounds() {
|
||||
testmap3_t t = {"kvtest"_n, "kvtest"_n, {{33, 10}, {10, 41.2f}, {11, 100.100f}, {2, 17.42f}}};
|
||||
|
||||
auto it = t.lower_bound(11);
|
||||
|
||||
eosio::check(it->key == 11, "should be pointing to 11");
|
||||
|
||||
it = t.lower_bound(31);
|
||||
|
||||
eosio::check(it->key == 33, "should be pointing to 33");
|
||||
|
||||
it = t.lower_bound(36);
|
||||
|
||||
eosio::check(it == t.end(), "should be pointing to end");
|
||||
|
||||
auto it2 = t.lower_bound(1);
|
||||
|
||||
eosio::check(it2->key == 2, "should be pointing to 2");
|
||||
|
||||
it = t.upper_bound(10);
|
||||
|
||||
eosio::check(it->key == 11, "should be pointing to 11");
|
||||
|
||||
it = t.upper_bound(33);
|
||||
|
||||
eosio::check(it == t.end(), "should be pointing to end");
|
||||
}
|
||||
|
||||
[[eosio::action]]
|
||||
void ranges() {
|
||||
testmap3_t t = {"kvtest"_n, "kvtest"_n, {{17, 9.9f}}};
|
||||
|
||||
auto range = t.equal_range(16);
|
||||
|
||||
eosio::check(range.first->key == 17, "should be pointing to 17");
|
||||
eosio::check(range.second->key == 17, "should be pointing to 17");
|
||||
|
||||
range = t.equal_range(1);
|
||||
|
||||
eosio::check(range.first->key == 2, "should be pointing to 2");
|
||||
eosio::check(range.second->key == 2, "should be pointing to 2");
|
||||
}
|
||||
|
||||
[[eosio::action]]
|
||||
void empty() {
|
||||
testmap_t t("kvtest"_n, "kvtest"_n);
|
||||
eosio::check(!t.empty(), "should be not empty");
|
||||
t.erase(t.begin(), t.end());
|
||||
eosio::check(t.empty(), "should be empty");
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user