From e77c18e74a08f45ebae5906d59b943313a4359e5 Mon Sep 17 00:00:00 2001 From: Dmytro Sydorchenko Date: Fri, 14 Apr 2023 20:04:59 -0400 Subject: [PATCH] multi_index bugfix after migration from boost + tests --- .../eosiolib/contracts/eosio/multi_index.hpp | 58 +- .../include/bluegrass/meta/for_each.hpp | 2 +- tests/integration/contracts.hpp.in | 3 + tests/integration/multi_index_tests.cpp | 75 ++ tests/unit/test_contracts/CMakeLists.txt | 1 + .../unit/test_contracts/multi_index_tests.cpp | 897 ++++++++++++++++++ 6 files changed, 1007 insertions(+), 29 deletions(-) create mode 100644 tests/integration/multi_index_tests.cpp create mode 100644 tests/unit/test_contracts/multi_index_tests.cpp diff --git a/libraries/eosiolib/contracts/eosio/multi_index.hpp b/libraries/eosiolib/contracts/eosio/multi_index.hpp index e73aa9d645..2b06384aac 100644 --- a/libraries/eosiolib/contracts/eosio/multi_index.hpp +++ b/libraries/eosiolib/contracts/eosio/multi_index.hpp @@ -753,6 +753,8 @@ class multi_index static auto extract_secondary_key(const T& obj) { return secondary_extractor_type()(obj); } + // used only for type deduction + constexpr index() : _multidx(nullptr) { } private: friend class multi_index; @@ -764,17 +766,19 @@ class multi_index template struct intc { enum e{ value = I }; operator uint64_t()const{ return I; } }; + enum index_cv { const_index = 0, mutable_index = 1 }; - template + template class make_index_tuple { - template + + template static constexpr auto get_type(std::index_sequence) { return std::make_tuple(std::make_tuple(index(Values::index_name)), typename Values::secondary_extractor_type, - intc::e::value, false>{}, + intc::e::value, const_index>{}, index(Values::index_name)), typename Values::secondary_extractor_type, - intc::e::value, true>{})...); + intc::e::value, mutable_index>{})...); } public: using type = decltype( get_type(std::make_index_sequence{}) ); @@ -783,19 +787,17 @@ class multi_index using indices_type = typename make_index_tuple::type; class make_extractor_tuple { - template - static constexpr auto get_type(const IndicesType& indices, const Obj& obj, std::index_sequence) { - return std::make_tuple(decltype(std::get<0>(std::get(indices)))::extract_secondary_key(obj)...); + template + static constexpr auto extractor_tuple(IndicesType, const Obj& obj, std::index_sequence) { + return std::make_tuple(std::tuple_element_t>::extract_secondary_key(obj)...); } public: template - static auto get_extractor_tuple(const IndicesType& indices, const Obj& obj) { - return get_type(indices, obj, std::make_index_sequence{}); + static constexpr auto get_extractor_tuple(IndicesType, const Obj& obj) { + return extractor_tuple(IndicesType{}, obj, std::make_index_sequence>{}); } }; - indices_type _indices; - const item& load_object_by_primary_iterator( int32_t itr )const { using namespace _multi_index_detail; @@ -820,8 +822,8 @@ class multi_index ds >> val; i.__primary_itr = itr; - bluegrass::meta::for_each(_indices, [&](auto& idx){ - typedef decltype(std::get<1>(idx)) index_type; + bluegrass::meta::for_each(indices_type{}, [&](auto idx){ + typedef std::tuple_element_t index_type; i.__iters[ index_type::number() ] = -1; }); }); @@ -1425,13 +1427,13 @@ class multi_index auto get_index() { using namespace _multi_index_detail; - constexpr uint64_t index_num = bluegrass::meta::for_each(_indices, [&](auto& idx){ - return decltype(std::get<0>(idx))::index_name == static_cast(IndexName); - }, _indices); + constexpr uint64_t index_num = bluegrass::meta::for_each(indices_type{}, [](auto idx){ + return std::tuple_element_t::index_name == static_cast(IndexName); + }); static_assert( index_num < sizeof...(Indices), "name provided is not the name of any secondary index within multi_index" ); - return decltype(std::get<0>(std::get(_indices)))(this); + return std::tuple_element_t>(this); } /** @@ -1476,13 +1478,13 @@ class multi_index auto get_index()const { using namespace _multi_index_detail; - constexpr uint64_t index_num = bluegrass::meta::for_each(_indices, [&](auto& idx){ - return decltype(std::get<1>(idx))::index_name == static_cast(IndexName); - }, _indices); + constexpr uint64_t index_num = bluegrass::meta::for_each(indices_type{}, [](auto idx){ + return std::tuple_element_t::index_name == static_cast(IndexName); + }); static_assert( index_num < sizeof...(Indices), "name provided is not the name of any secondary index within multi_index" ); - return decltype(std::get<1>(std::get(_indices)))(this); + return std::tuple_element_t>(this); } /** @@ -1595,8 +1597,8 @@ class multi_index if( pk >= _next_primary_key ) _next_primary_key = (pk >= no_available_primary_key) ? no_available_primary_key : (pk + 1); - bluegrass::meta::for_each(_indices, [&](auto& idx){ - typedef decltype(std::get<0>(idx)) index_type; + bluegrass::meta::for_each(indices_type{}, [&](auto idx){ + typedef std::tuple_element_t index_type; i.__iters[index_type::number()] = secondary_index_db_functions::db_idx_store( _scope, index_type::name(), payer.value, obj.primary_key(), index_type::extract_secondary_key(obj) ); }); @@ -1708,7 +1710,7 @@ class multi_index auto& mutableitem = const_cast(objitem); eosio::check( _code == current_receiver(), "cannot modify objects in table of another contract" ); // Quick fix for mutating db using multi_index that shouldn't allow mutation. Real fix can come in RC2. - auto secondary_keys = make_extractor_tuple::get_extractor_tuple(_indices, obj); + auto secondary_keys = make_extractor_tuple::get_extractor_tuple(indices_type{}, obj); uint64_t pk = _multi_index_detail::to_raw_key(obj.primary_key()); @@ -1733,8 +1735,8 @@ class multi_index if( pk >= _next_primary_key ) _next_primary_key = (pk >= no_available_primary_key) ? no_available_primary_key : (pk + 1); - bluegrass::meta::for_each(_indices, [&](auto& idx){ - typedef decltype(std::get<0>(idx)) index_type; + bluegrass::meta::for_each(indices_type{}, [&](auto idx){ + typedef std::tuple_element_t index_type; auto secondary = index_type::extract_secondary_key( obj ); if( memcmp( &std::get(secondary_keys), &secondary, sizeof(secondary) ) != 0 ) { auto indexitr = mutableitem.__iters[index_type::number()]; @@ -1948,8 +1950,8 @@ class multi_index internal_use_do_not_use::db_remove_i64( objitem.__primary_itr ); - bluegrass::meta::for_each(_indices, [&](auto& idx){ - typedef decltype(std::get<0>(idx)) index_type; + bluegrass::meta::for_each(indices_type{}, [&](auto idx){ + typedef std::tuple_element_t index_type; auto i = objitem.__iters[index_type::number()]; if( i < 0 ) { diff --git a/libraries/meta_refl/include/bluegrass/meta/for_each.hpp b/libraries/meta_refl/include/bluegrass/meta/for_each.hpp index 89b35146be..9bf7020c70 100644 --- a/libraries/meta_refl/include/bluegrass/meta/for_each.hpp +++ b/libraries/meta_refl/include/bluegrass/meta/for_each.hpp @@ -876,7 +876,7 @@ void for_each_field(T&& value, F&& func) { template typename Tuple, typename Fn, typename... Ts> constexpr size_t for_each_impl(Tuple& tup, Fn fn) { if constexpr(I < sizeof...(Ts)) { - using result_t = std::decay_t(tup)))>; + using result_t = decltype(fn(std::get(tup))); if constexpr (std::is_same_v) { fn(std::get(tup)); } diff --git a/tests/integration/contracts.hpp.in b/tests/integration/contracts.hpp.in index 185df34dfd..6fea3a3d08 100644 --- a/tests/integration/contracts.hpp.in +++ b/tests/integration/contracts.hpp.in @@ -30,5 +30,8 @@ namespace eosio::testing { static std::vector name_pk_tests_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/name_pk_tests.wasm"); } static std::vector name_pk_tests_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/name_pk_tests.abi"); } + + static std::vector test_multi_index_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/test_multi_index.wasm"); } + static std::vector test_multi_index_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/test_multi_index.abi"); } }; } //ns eosio::testing diff --git a/tests/integration/multi_index_tests.cpp b/tests/integration/multi_index_tests.cpp new file mode 100644 index 0000000000..c5420a77ff --- /dev/null +++ b/tests/integration/multi_index_tests.cpp @@ -0,0 +1,75 @@ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-compare" +#include +#pragma GCC diagnostic pop + +#include + +#include + +using namespace eosio; +using namespace eosio::testing; + +#ifdef NON_VALIDATING_TEST +#define TESTER tester +#else +#define TESTER validating_tester +#endif + +BOOST_AUTO_TEST_SUITE(multi_index_tests) + +// this test is copy from leap test_api_multi_index +BOOST_FIXTURE_TEST_CASE(main_multi_index_tests, TESTER) { try { + produce_blocks(1); + create_account( "testapi"_n ); + produce_blocks(1); + set_code( "testapi"_n, contracts::test_multi_index_wasm() ); + set_abi( "testapi"_n, contracts::test_multi_index_abi().data() ); + produce_blocks(1); + + auto check_failure = [this]( action_name a, const char* expected_error_msg ) { + BOOST_CHECK_EXCEPTION( push_action( "testapi"_n, a, "testapi"_n, {} ), + eosio_assert_message_exception, + eosio_assert_message_is( expected_error_msg ) + ); + }; + + push_action( "testapi"_n, "s1g"_n, "testapi"_n, {} ); // idx64_general + push_action( "testapi"_n, "s1store"_n, "testapi"_n, {} ); // idx64_store_only + push_action( "testapi"_n, "s1check"_n, "testapi"_n, {} ); // idx64_check_without_storing + push_action( "testapi"_n, "s2g"_n, "testapi"_n, {} ); // idx128_general + push_action( "testapi"_n, "s2store"_n, "testapi"_n, {} ); // idx128_store_only + push_action( "testapi"_n, "s2check"_n, "testapi"_n, {} ); // idx128_check_without_storing + push_action( "testapi"_n, "s2autoinc"_n, "testapi"_n, {} ); // idx128_autoincrement_test + push_action( "testapi"_n, "s2autoinc1"_n, "testapi"_n, {} ); // idx128_autoincrement_test_part1 + push_action( "testapi"_n, "s2autoinc2"_n, "testapi"_n, {} ); // idx128_autoincrement_test_part2 + push_action( "testapi"_n, "s3g"_n, "testapi"_n, {} ); // idx256_general + push_action( "testapi"_n, "sdg"_n, "testapi"_n, {} ); // idx_double_general + push_action( "testapi"_n, "sldg"_n, "testapi"_n, {} ); // idx_long_double_general + + check_failure( "s1pkend"_n, "cannot increment end iterator" ); // idx64_pk_iterator_exceed_end + check_failure( "s1skend"_n, "cannot increment end iterator" ); // idx64_sk_iterator_exceed_end + check_failure( "s1pkbegin"_n, "cannot decrement iterator at beginning of table" ); // idx64_pk_iterator_exceed_begin + check_failure( "s1skbegin"_n, "cannot decrement iterator at beginning of index" ); // idx64_sk_iterator_exceed_begin + check_failure( "s1pkref"_n, "object passed to iterator_to is not in multi_index" ); // idx64_pass_pk_ref_to_other_table + check_failure( "s1skref"_n, "object passed to iterator_to is not in multi_index" ); // idx64_pass_sk_ref_to_other_table + check_failure( "s1pkitrto"_n, "object passed to iterator_to is not in multi_index" ); // idx64_pass_pk_end_itr_to_iterator_to + check_failure( "s1pkmodify"_n, "cannot pass end iterator to modify" ); // idx64_pass_pk_end_itr_to_modify + check_failure( "s1pkerase"_n, "cannot pass end iterator to erase" ); // idx64_pass_pk_end_itr_to_erase + check_failure( "s1skitrto"_n, "object passed to iterator_to is not in multi_index" ); // idx64_pass_sk_end_itr_to_iterator_to + check_failure( "s1skmodify"_n, "cannot pass end iterator to modify" ); // idx64_pass_sk_end_itr_to_modify + check_failure( "s1skerase"_n, "cannot pass end iterator to erase" ); // idx64_pass_sk_end_itr_to_erase + check_failure( "s1modpk"_n, "updater cannot change primary key when modifying an object" ); // idx64_modify_primary_key + check_failure( "s1exhaustpk"_n, "next primary key in table is at autoincrement limit" ); // idx64_run_out_of_avl_pk + check_failure( "s1findfail1"_n, "unable to find key" ); // idx64_require_find_fail + check_failure( "s1findfail2"_n, "unable to find primary key in require_find" );// idx64_require_find_fail_with_msg + check_failure( "s1findfail3"_n, "unable to find secondary key" ); // idx64_require_find_sk_fail + check_failure( "s1findfail4"_n, "unable to find sec key" ); // idx64_require_find_sk_fail_with_msg + + push_action( "testapi"_n, "s1skcache"_n, "testapi"_n, {} ); // idx64_sk_cache_pk_lookup + push_action( "testapi"_n, "s1pkcache"_n, "testapi"_n, {} ); // idx64_pk_cache_sk_lookup + + BOOST_REQUIRE_EQUAL( validate(), true ); +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/tests/unit/test_contracts/CMakeLists.txt b/tests/unit/test_contracts/CMakeLists.txt index 2139b1c7ae..48dc55e7e7 100644 --- a/tests/unit/test_contracts/CMakeLists.txt +++ b/tests/unit/test_contracts/CMakeLists.txt @@ -10,6 +10,7 @@ add_contract(crypto_primitives_tests crypto_primitives_tests crypto_primitives_t add_contract(get_code_hash_tests get_code_hash_write get_code_hash_write.cpp) add_contract(get_code_hash_tests get_code_hash_read get_code_hash_read.cpp) add_contract(name_pk_tests name_pk_tests name_pk_tests.cpp) +add_contract(test_multi_index test_multi_index multi_index_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) diff --git a/tests/unit/test_contracts/multi_index_tests.cpp b/tests/unit/test_contracts/multi_index_tests.cpp new file mode 100644 index 0000000000..8c62358081 --- /dev/null +++ b/tests/unit/test_contracts/multi_index_tests.cpp @@ -0,0 +1,897 @@ +#pragma once + +#include + +#include +#include + +namespace _test_multi_index +{ + + using eosio::checksum256; + + struct record_idx64 + { + uint64_t id; + uint64_t sec; + + auto primary_key() const { return id; } + uint64_t get_secondary() const { return sec; } + + EOSLIB_SERIALIZE(record_idx64, (id)(sec)) + }; + + struct record_idx128 + { + uint64_t id; + uint128_t sec; + + auto primary_key() const { return id; } + uint128_t get_secondary() const { return sec; } + + EOSLIB_SERIALIZE(record_idx128, (id)(sec)) + }; + + struct record_idx256 + { + uint64_t id; + checksum256 sec; + + auto primary_key() const { return id; } + const checksum256 &get_secondary() const { return sec; } + + EOSLIB_SERIALIZE(record_idx256, (id)(sec)) + }; + + struct record_idx_double + { + uint64_t id; + double sec; + + auto primary_key() const { return id; } + double get_secondary() const { return sec; } + + EOSLIB_SERIALIZE(record_idx_double, (id)(sec)) + }; + + struct record_idx_long_double + { + uint64_t id; + long double sec; + + auto primary_key() const { return id; } + long double get_secondary() const { return sec; } + + EOSLIB_SERIALIZE(record_idx_long_double, (id)(sec)) + }; + + template + void idx64_store_only(eosio::name receiver) + { + typedef record_idx64 record; + + record records[] = {{265, "alice"_n.value}, + {781, "bob"_n.value}, + {234, "charlie"_n.value}, + {650, "allyson"_n.value}, + {540, "bob"_n.value}, + {976, "emily"_n.value}, + {110, "joe"_n.value}}; + size_t num_records = sizeof(records) / sizeof(records[0]); + + // Construct and fill table using multi_index + eosio::multi_index>> + table(receiver, receiver.value); + + auto payer = receiver; + + for (size_t i = 0; i < num_records; ++i) + { + table.emplace(payer, [&](auto &r) + { + r.id = records[i].id; + r.sec = records[i].sec; }); + } + } + + template + void idx64_check_without_storing(eosio::name receiver) + { + typedef record_idx64 record; + + // Load table using multi_index + eosio::multi_index>> + table(receiver, receiver.value); + + auto payer = receiver; + + auto secondary_index = table.template get_index<"bysecondary"_n>(); + + // find by primary key + { + auto itr = table.find(999); + eosio::check(itr == table.end(), "idx64_general - table.find() of non-existing primary key"); + + itr = table.find(976); + eosio::check(itr != table.end() && itr->sec == "emily"_n.value, "idx64_general - table.find() of existing primary key"); + + ++itr; + eosio::check(itr == table.end(), "idx64_general - increment primary iterator to end"); + + itr = table.require_find(976); + eosio::check(itr != table.end() && itr->sec == "emily"_n.value, "idx64_general - table.require_find() of existing primary key"); + + ++itr; + eosio::check(itr == table.end(), "idx64_general - increment primary iterator to end"); + } + + // iterate forward starting with charlie + { + auto itr = secondary_index.lower_bound("charlie"_n.value); + eosio::check(itr != secondary_index.end() && itr->sec == "charlie"_n.value, "idx64_general - secondary_index.lower_bound()"); + + ++itr; + eosio::check(itr != secondary_index.end() && itr->id == 976 && itr->sec == "emily"_n.value, "idx64_general - increment secondary iterator"); + + ++itr; + eosio::check(itr != secondary_index.end() && itr->id == 110 && itr->sec == "joe"_n.value, "idx64_general - increment secondary iterator again"); + + ++itr; + eosio::check(itr == secondary_index.end(), "idx64_general - increment secondary iterator to end"); + } + + // iterate backward starting with second bob + { + auto pk_itr = table.find(781); + eosio::check(pk_itr != table.end() && pk_itr->sec == "bob"_n.value, "idx64_general - table.find() of existing primary key"); + + auto itr = secondary_index.iterator_to(*pk_itr); + eosio::check(itr->id == 781 && itr->sec == "bob"_n.value, "idx64_general - iterator to existing object in secondary index"); + + --itr; + eosio::check(itr != secondary_index.end() && itr->id == 540 && itr->sec == "bob"_n.value, "idx64_general - decrement secondary iterator"); + + --itr; + eosio::check(itr != secondary_index.end() && itr->id == 650 && itr->sec == "allyson"_n.value, "idx64_general - decrement secondary iterator again"); + + --itr; + eosio::check(itr == secondary_index.begin() && itr->id == 265 && itr->sec == "alice"_n.value, "idx64_general - decrement secondary iterator to beginning"); + } + + // iterate backward starting with emily using const_reverse_iterator + { + std::array pks{{976, 234, 781, 540, 650, 265}}; + + auto pk_itr = pks.begin(); + + auto itr = --std::make_reverse_iterator(secondary_index.find("emily"_n.value)); + for (; itr != secondary_index.rend(); ++itr) + { + eosio::check(pk_itr != pks.end(), "idx64_general - unexpected continuation of secondary index in reverse iteration"); + eosio::check(*pk_itr == itr->id, "idx64_general - primary key mismatch in reverse iteration"); + ++pk_itr; + } + eosio::check(pk_itr == pks.end(), "idx64_general - did not iterate backwards through secondary index properly"); + } + + // require_find secondary key + { + auto itr = secondary_index.require_find("bob"_n.value); + eosio::check(itr != secondary_index.end(), "idx64_general - require_find must never return end iterator"); + eosio::check(itr->id == 540, "idx64_general - require_find test"); + + ++itr; + eosio::check(itr->id == 781, "idx64_general - require_find secondary key test"); + } + + // modify and erase + { + const uint64_t ssn = 421; + auto new_person = table.emplace(payer, [&](auto &r) + { + r.id = ssn; + r.sec = "bob"_n.value; }); + + table.modify(new_person, payer, [&](auto &r) + { r.sec = "billy"_n.value; }); + + auto itr1 = table.find(ssn); + eosio::check(itr1 != table.end() && itr1->sec == "billy"_n.value, "idx64_general - table.modify()"); + + table.erase(itr1); + auto itr2 = table.find(ssn); + eosio::check(itr2 == table.end(), "idx64_general - table.erase()"); + } + } + + template + void idx64_require_find_fail(eosio::name receiver) + { + typedef record_idx64 record; + + // Load table using multi_index + eosio::multi_index table(receiver, receiver.value); + + // make sure we're looking at the right table + auto itr = table.require_find(781, "table not loaded"); + eosio::check(itr != table.end(), "table not loaded"); + + // require_find by primary key + // should fail + itr = table.require_find(999); + } + + template + void idx64_require_find_fail_with_msg(eosio::name receiver) + { + typedef record_idx64 record; + + // Load table using multi_index + eosio::multi_index table(receiver, receiver.value); + + // make sure we're looking at the right table + auto itr = table.require_find(234, "table not loaded"); + eosio::check(itr != table.end(), "table not loaded"); + + // require_find by primary key + // should fail + itr = table.require_find(335, "unable to find primary key in require_find"); + } + + template + void idx64_require_find_sk_fail(eosio::name receiver) + { + typedef record_idx64 record; + + // Load table using multi_index + eosio::multi_index>> table(receiver, receiver.value); + auto sec_index = table.template get_index<"bysecondary"_n>(); + + // make sure we're looking at the right table + auto itr = sec_index.require_find("charlie"_n.value, "table not loaded"); + eosio::check(itr != sec_index.end(), "table not loaded"); + + // require_find by secondary key + // should fail + itr = sec_index.require_find("bill"_n.value); + } + + template + void idx64_require_find_sk_fail_with_msg(eosio::name receiver) + { + typedef record_idx64 record; + + // Load table using multi_index + eosio::multi_index>> table(receiver, receiver.value); + auto sec_index = table.template get_index<"bysecondary"_n>(); + + // make sure we're looking at the right table + auto itr = sec_index.require_find("emily"_n.value, "table not loaded"); + eosio::check(itr != sec_index.end(), "table not loaded"); + + // require_find by secondary key + // should fail + itr = sec_index.require_find("frank"_n.value, "unable to find sec key"); + } + + template + void idx128_store_only(eosio::name receiver) + { + typedef record_idx128 record; + + // Construct and fill table using multi_index + eosio::multi_index>> + table(receiver, receiver.value); + + auto payer = receiver; + + for (uint64_t i = 0; i < 5; ++i) + { + table.emplace(payer, [&](auto &r) + { + r.id = i; + r.sec = static_cast(1ULL << 63) * i; }); + } + } + + template + void idx128_check_without_storing(eosio::name receiver) + { + typedef record_idx128 record; + + // Load table using multi_index + eosio::multi_index>> + table(receiver, receiver.value); + + auto payer = receiver; + + auto secondary_index = table.template get_index<"bysecondary"_n>(); + + table.modify(table.get(3), payer, [&](auto &r) + { r.sec *= 2; }); + + { + uint128_t multiplier = 1ULL << 63; + + auto itr = secondary_index.begin(); + eosio::check(itr->primary_key() == 0 && itr->get_secondary() == multiplier * 0, "idx128_general - secondary key sort"); + ++itr; + eosio::check(itr->primary_key() == 1 && itr->get_secondary() == multiplier * 1, "idx128_general - secondary key sort"); + ++itr; + eosio::check(itr->primary_key() == 2 && itr->get_secondary() == multiplier * 2, "idx128_general - secondary key sort"); + ++itr; + eosio::check(itr->primary_key() == 4 && itr->get_secondary() == multiplier * 4, "idx128_general - secondary key sort"); + ++itr; + eosio::check(itr->primary_key() == 3 && itr->get_secondary() == multiplier * 6, "idx128_general - secondary key sort"); + ++itr; + eosio::check(itr == secondary_index.end(), "idx128_general - secondary key sort"); + } + } + + template + auto idx64_table(eosio::name receiver) + { + typedef record_idx64 record; + // Load table using multi_index + eosio::multi_index>> + table(receiver, receiver.value); + return table; + } + +} /// _test_multi_index + +class [[eosio::contract]] test_multi_index : public eosio::contract +{ +public: + using eosio::contract::contract; + + [[eosio::action("s1g")]] void idx64_general() { + _test_multi_index::idx64_store_only<"indextable2"_n.value>( get_self() ); + _test_multi_index::idx64_check_without_storing<"indextable2"_n.value>( get_self() ); + } + + [[eosio::action("s1store")]] void idx64_store_only() { + _test_multi_index::idx64_store_only<"indextable1"_n.value>(get_self()); + } + + [[eosio::action("s1check")]] void idx64_check_without_storing() { + _test_multi_index::idx64_check_without_storing<"indextable1"_n.value>( get_self() ); + } + + [[eosio::action("s1findfail1")]] void idx64_require_find_fail() { + _test_multi_index::idx64_store_only<"indextable5"_n.value>( get_self() ); + _test_multi_index::idx64_require_find_fail<"indextable5"_n.value>( get_self() ); + } + + [[eosio::action("s1findfail2")]] void idx64_require_find_fail_with_msg() { + _test_multi_index::idx64_store_only<"indextablea"_n.value>( get_self() ); // Making the name smaller fixes this? + _test_multi_index::idx64_require_find_fail_with_msg<"indextablea"_n.value>( get_self() ); // Making the name smaller fixes this? + } + + [[eosio::action("s1findfail3")]] void idx64_require_find_sk_fail() { + _test_multi_index::idx64_store_only<"indextableb"_n.value>( get_self() ); + _test_multi_index::idx64_require_find_sk_fail<"indextableb"_n.value>( get_self() ); + } + + [[eosio::action("s1findfail4")]] void idx64_require_find_sk_fail_with_msg() { + _test_multi_index::idx64_store_only<"indextablec"_n.value>( get_self() ); + _test_multi_index::idx64_require_find_sk_fail_with_msg<"indextablec"_n.value>( get_self() ); + } + + [[eosio::action("s1pkend")]] void idx64_pk_iterator_exceed_end() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + auto end_itr = table.end(); + // Should fail + ++end_itr; + } + + [[eosio::action("s1skend")]] void idx64_sk_iterator_exceed_end() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + auto end_itr = table.get_index<"bysecondary"_n>().end(); + // Should fail + ++end_itr; + } + + [[eosio::action("s1pkbegin")]] void idx64_pk_iterator_exceed_begin() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + auto begin_itr = table.begin(); + // Should fail + --begin_itr; + } + + [[eosio::action("s1skbegin")]] void idx64_sk_iterator_exceed_begin() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + auto begin_itr = table.get_index<"bysecondary"_n>().begin(); + // Should fail + --begin_itr; + } + + [[eosio::action("s1pkref")]] void idx64_pass_pk_ref_to_other_table() { + auto table1 = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + auto table2 = _test_multi_index::idx64_table<"indextable2"_n.value, "bysecondary"_n.value>( get_self() ); + + auto table1_pk_itr = table1.find(781); + eosio::check( table1_pk_itr != table1.end() && table1_pk_itr->sec == "bob"_n.value, "idx64_pass_pk_ref_to_other_table - table.find() of existing primary key" ); + + // Should fail + table2.iterator_to(*table1_pk_itr); + } + + [[eosio::action("s1skref")]] void idx64_pass_sk_ref_to_other_table() { + auto table1 = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + auto table2 = _test_multi_index::idx64_table<"indextable2"_n.value, "bysecondary"_n.value>( get_self() ); + + auto table1_pk_itr = table1.find(781); + eosio::check( table1_pk_itr != table1.end() && table1_pk_itr->sec == "bob"_n.value, "idx64_pass_sk_ref_to_other_table - table.find() of existing primary key" ); + + auto table2_sec_index = table2.get_index<"bysecondary"_n>(); + // Should fail + table2_sec_index.iterator_to(*table1_pk_itr); + } + + [[eosio::action("s1pkitrto")]] void idx64_pass_pk_end_itr_to_iterator_to() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + auto end_itr = table.end(); + // Should fail + table.iterator_to(*end_itr); + } + + [[eosio::action("s1pkmodify")]] void idx64_pass_pk_end_itr_to_modify() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + auto end_itr = table.end(); + + // Should fail + table.modify( end_itr, get_self(), [](auto&){} ); + } + + [[eosio::action("s1pkerase")]] void idx64_pass_pk_end_itr_to_erase() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + auto end_itr = table.end(); + + // Should fail + table.erase(end_itr); + } + + [[eosio::action("s1skitrto")]] void idx64_pass_sk_end_itr_to_iterator_to() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + auto sec_index = table.get_index<"bysecondary"_n>(); + auto end_itr = sec_index.end(); + + // Should fail + sec_index.iterator_to(*end_itr); + } + + [[eosio::action("s1skmodify")]] void idx64_pass_sk_end_itr_to_modify() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + auto sec_index = table.get_index<"bysecondary"_n>(); + auto end_itr = sec_index.end(); + + // Should fail + sec_index.modify( end_itr, get_self(), [](auto&){} ); + } + + [[eosio::action("s1skerase")]] void idx64_pass_sk_end_itr_to_erase() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + auto sec_index = table.get_index<"bysecondary"_n>(); + auto end_itr = sec_index.end(); + + // Should fail + sec_index.erase(end_itr); + } + + [[eosio::action("s1modpk")]] void idx64_modify_primary_key() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + + auto pk_itr = table.find(781); + eosio::check( pk_itr != table.end() && pk_itr->sec == "bob"_n.value, "idx64_modify_primary_key - table.find() of existing primary key" ); + + // Should fail + table.modify( pk_itr, get_self(), [](auto& r){ + r.id = 1100; + }); + } + + [[eosio::action("s1exhaustpk")]] void idx64_run_out_of_avl_pk() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + + auto pk_itr = table.find(781); + eosio::check( pk_itr != table.end() && pk_itr->sec == "bob"_n.value, "idx64_modify_primary_key - table.find() of existing primary key" ); + + auto payer = get_self(); + + table.emplace( payer, [&](auto& r) { + r.id = static_cast(-4); + r.sec = "alice"_n.value; + }); + eosio::check( table.available_primary_key() == static_cast(-3), "idx64_run_out_of_avl_pk - incorrect available primary key" ); + + table.emplace( payer, [&](auto& r) { + r.id = table.available_primary_key(); + r.sec = "bob"_n.value; + }); + + // Should fail + table.available_primary_key(); + } + + [[eosio::action("s1skcache")]] void idx64_sk_cache_pk_lookup() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + + auto sec_index = table.get_index<"bysecondary"_n>(); + auto sk_itr = sec_index.find("bob"_n.value); + eosio::check( sk_itr != sec_index.end() && sk_itr->id == 540, "idx64_sk_cache_pk_lookup - sec_index.find() of existing secondary key" ); + + auto pk_itr = table.iterator_to(*sk_itr); + auto prev_itr = --pk_itr; + eosio::check( prev_itr->id == 265 && prev_itr->sec == "alice"_n.value, "idx64_sk_cache_pk_lookup - previous record" ); + } + + [[eosio::action("s1pkcache")]] void idx64_pk_cache_sk_lookup() { + auto table = _test_multi_index::idx64_table<"indextable1"_n.value, "bysecondary"_n.value>( get_self() ); + + + auto pk_itr = table.find(540); + eosio::check( pk_itr != table.end() && pk_itr->sec == "bob"_n.value, "idx64_pk_cache_sk_lookup - table.find() of existing primary key" ); + + auto sec_index = table.get_index<"bysecondary"_n>(); + auto sk_itr = sec_index.iterator_to(*pk_itr); + auto next_itr = ++sk_itr; + eosio::check( next_itr->id == 781 && next_itr->sec == "bob"_n.value, "idx64_pk_cache_sk_lookup - next record" ); + } + + [[eosio::action("s2g")]] void idx128_general() { + _test_multi_index::idx128_store_only<"indextable4"_n.value>( get_self() ); + _test_multi_index::idx128_check_without_storing<"indextable4"_n.value>( get_self() ); + } + + [[eosio::action("s2store")]] void idx128_store_only() { + _test_multi_index::idx128_store_only<"indextable3"_n.value>( get_self() ); + } + + [[eosio::action("s2check")]] void idx128_check_without_storing() { + _test_multi_index::idx128_check_without_storing<"indextable3"_n.value>( get_self() ); + } + + [[eosio::action("s2autoinc")]] void idx128_autoincrement_test() { + using namespace _test_multi_index; + + typedef record_idx128 record; + + auto payer = get_self(); + + eosio::multi_index<"autoinctbl1"_n, record, + eosio::indexed_by<"bysecondary"_n, eosio::const_mem_fun> + > table( get_self(), get_self().value ); + + for( int i = 0; i < 5; ++i ) { + table.emplace( payer, [&](auto& r) { + r.id = table.available_primary_key(); + r.sec = 1000 - static_cast(r.id); + }); + } + + uint64_t expected_key = 4; + for( const auto& r : table.get_index<"bysecondary"_n>() ) + { + eosio::check( r.primary_key() == expected_key, "idx128_autoincrement_test - unexpected primary key" ); + --expected_key; + } + eosio::check( expected_key == static_cast(-1), "idx128_autoincrement_test - did not iterate through secondary index properly" ); + + auto itr = table.find(3); + eosio::check( itr != table.end(), "idx128_autoincrement_test - could not find object with primary key of 3" ); + + // The modification below would trigger an error: + /* + table.modify(itr, payer, [&](auto& r) { + r.id = 100; + }); + */ + + table.emplace( payer, [&](auto& r) { + r.id = 100; + r.sec = itr->sec; + }); + table.erase(itr); + + eosio::check( table.available_primary_key() == 101, "idx128_autoincrement_test - next_primary_key was not correct after record modify" ); + } + + [[eosio::action("s2autoinc1")]] void idx128_autoincrement_test_part1() { + using namespace _test_multi_index; + + typedef record_idx128 record; + + auto payer = get_self(); + + eosio::multi_index<"autoinctbl2"_n, record, + eosio::indexed_by<"bysecondary"_n, eosio::const_mem_fun> + > table( get_self(), get_self().value ); + + for( int i = 0; i < 3; ++i ) { + table.emplace( payer, [&](auto& r) { + r.id = table.available_primary_key(); + r.sec = 1000 - static_cast(r.id); + }); + } + + table.erase(table.get(0)); + + uint64_t expected_key = 2; + for( const auto& r : table.get_index<"bysecondary"_n>() ) + { + eosio::check( r.primary_key() == expected_key, "idx128_autoincrement_test_part1 - unexpected primary key" ); + --expected_key; + } + eosio::check( expected_key == 0, "idx128_autoincrement_test_part1 - did not iterate through secondary index properly" ); + } + + [[eosio::action("s2autoinc2")]] void idx128_autoincrement_test_part2() { + using namespace _test_multi_index; + + typedef record_idx128 record; + + const eosio::name::raw table_name = "autoinctbl2"_n; + auto payer = get_self(); + + { + eosio::multi_index> + > table( get_self(), get_self().value ); + + eosio::check( table.available_primary_key() == 3, "idx128_autoincrement_test_part2 - did not recover expected next primary key" ); + } + + eosio::multi_index> + > table( get_self(), get_self().value ); + + table.emplace( payer, [&](auto& r) { + r.id = 0; + r.sec = 1000; + }); + // Done this way to make sure that table._next_primary_key is not incorrectly set to 1. + + for( int i = 3; i < 5; ++i ) { + table.emplace( payer, [&](auto& r) { + auto itr = table.available_primary_key(); + r.id = itr; + r.sec = 1000 - static_cast(r.id); + }); + } + + uint64_t expected_key = 4; + for( const auto& r : table.get_index<"bysecondary"_n>() ) + { + eosio::check( r.primary_key() == expected_key, "idx128_autoincrement_test_part2 - unexpected primary key" ); + --expected_key; + } + eosio::check( expected_key == static_cast(-1), "idx128_autoincrement_test_part2 - did not iterate through secondary index properly" ); + + auto itr = table.find(3); + eosio::check( itr != table.end(), "idx128_autoincrement_test_part2 - could not find object with primary key of 3" ); + + table.emplace( payer, [&](auto& r) { + r.id = 100; + r.sec = itr->sec; + }); + table.erase(itr); + + eosio::check( table.available_primary_key() == 101, "idx128_autoincrement_test_part2 - next_primary_key was not correct after record update" ); + } + + [[eosio::action("s3g")]] void idx256_general() { + using namespace _test_multi_index; + + typedef record_idx256 record; + + auto payer = get_self(); + + eosio::print("Testing checksum256 secondary index.\n"); + eosio::multi_index<"indextable5"_n, record, + eosio::indexed_by<"bysecondary"_n, eosio::const_mem_fun> + > table( get_self(), get_self().value ); + + auto fourtytwo = checksum256::make_from_word_sequence( 0ULL, 0ULL, 0ULL, 42ULL ); + //auto onetwothreefour = checksum256::make_from_word_sequence(1ULL, 2ULL, 3ULL, 4ULL); + auto onetwothreefour = checksum256{std::array{ {0,1, 0,2, 0,3, 0,4} }}; + + table.emplace( payer, [&](auto& o) { + o.id = 1; + o.sec = fourtytwo; + }); + + table.emplace( payer, [&](auto& o) { + o.id = 2; + o.sec = onetwothreefour; + }); + + table.emplace( payer, [&](auto& o) { + o.id = 3; + o.sec = fourtytwo; + }); + + auto e = table.find(2); + + eosio::print("Items sorted by primary key:\n"); + for( const auto& item : table ) { + eosio::print(" ID=", item.primary_key(), ", secondary=", item.sec, "\n"); + } + + { + auto itr = table.begin(); + eosio::check( itr->primary_key() == 1 && itr->get_secondary() == fourtytwo, "idx256_general - primary key sort" ); + ++itr; + eosio::check( itr->primary_key() == 2 && itr->get_secondary() == onetwothreefour, "idx256_general - primary key sort" ); + ++itr; + eosio::check( itr->primary_key() == 3 && itr->get_secondary() == fourtytwo, "idx256_general - primary key sort" ); + ++itr; + eosio::check( itr == table.end(), "idx256_general - primary key sort" ); + } + + auto secidx = table.get_index<"bysecondary"_n>(); + + auto lower1 = secidx.lower_bound( checksum256::make_from_word_sequence(0ULL, 0ULL, 0ULL, 40ULL) ); + eosio::print("First entry with a secondary key of at least 40 has ID=", lower1->id, ".\n"); + eosio::check( lower1->id == 1, "idx256_general - lower_bound" ); + + auto lower2 = secidx.lower_bound( checksum256::make_from_word_sequence(0ULL, 0ULL, 0ULL, 50ULL) ); + eosio::print("First entry with a secondary key of at least 50 has ID=", lower2->id, ".\n"); + eosio::check( lower2->id == 2, "idx256_general - lower_bound" ); + + if( table.iterator_to(*lower2) == e ) { + eosio::print("Previously found entry is the same as the one found earlier with a primary key value of 2.\n"); + } + + eosio::print("Items sorted by secondary key (checksum256):\n"); + for( const auto& item : secidx ) { + eosio::print(" ID=", item.primary_key(), ", secondary=", item.sec, "\n"); + } + + { + auto itr = secidx.begin(); + eosio::check( itr->primary_key() == 1, "idx256_general - secondary key sort" ); + ++itr; + eosio::check( itr->primary_key() == 3, "idx256_general - secondary key sort" ); + ++itr; + eosio::check( itr->primary_key() == 2, "idx256_general - secondary key sort" ); + ++itr; + eosio::check( itr == secidx.end(), "idx256_general - secondary key sort" ); + } + + auto upper = secidx.upper_bound( checksum256{std::array{{0, 0, 0, 42}}} ); + + eosio::print("First entry with a secondary key greater than 42 has ID=", upper->id, ".\n"); + eosio::check( upper->id == 2, "idx256_general - upper_bound" ); + eosio::check( upper->id == secidx.get(onetwothreefour).id, "idx256_general - secondary index get" ); + + eosio::print("Removed entry with ID=", lower1->id, ".\n"); + secidx.erase( lower1 ); + + eosio::print("Items reverse sorted by primary key:\n"); + for( auto itr = table.rbegin(); itr != table.rend(); ++itr ) { + const auto& item = *itr; + eosio::print(" ID=", item.primary_key(), ", secondary=", item.sec, "\n"); + } + + { + auto itr = table.rbegin(); + eosio::check( itr->primary_key() == 3 && itr->get_secondary() == fourtytwo, "idx256_general - primary key sort after remove" ); + ++itr; + eosio::check( itr->primary_key() == 2 && itr->get_secondary() == onetwothreefour, "idx256_general - primary key sort after remove" ); + ++itr; + eosio::check( itr == table.rend(), "idx256_general - primary key sort after remove" ); + } + } + + [[eosio::action("sdg")]] void idx_double_general() { + using namespace _test_multi_index; + + typedef record_idx_double record; + + auto payer = get_self(); + + eosio::print("Testing double secondary index.\n"); + eosio::multi_index<"floattable1"_n, record, + eosio::indexed_by<"bysecondary"_n, eosio::const_mem_fun> + > table( get_self(), get_self().value ); + + auto secidx = table.get_index<"bysecondary"_n>(); + + double tolerance = std::numeric_limits::epsilon(); + eosio::print("tolerance = ", tolerance, "\n"); + + for( uint64_t i = 1; i <= 10; ++i ) { + table.emplace( payer, [&]( auto& o ) { + o.id = i; + o.sec = 1.0 / (i * 1000000.0); + }); + } + + double expected_product = 1.0 / 1000000.0; + eosio::print( "expected_product = ", expected_product, "\n" ); + + uint64_t expected_key = 10; + for( const auto& obj : secidx ) { + eosio::check( obj.primary_key() == expected_key, "idx_double_general - unexpected primary key" ); + + double prod = obj.sec * obj.id; + + eosio::print(" id = ", obj.id, ", sec = ", obj.sec, ", sec * id = ", prod, "\n"); + + eosio::check( std::abs(prod - expected_product) <= tolerance, + "idx_double_general - product of secondary and id not equal to expected_product within tolerance" ); + + --expected_key; + } + eosio::check( expected_key == 0, "idx_double_general - did not iterate through secondary index properly" ); + + { + auto itr = secidx.lower_bound( expected_product / 5.5 ); + eosio::check( std::abs(1.0 / itr->sec - 5000000.0) <= tolerance, "idx_double_general - lower_bound" ); + + itr = secidx.upper_bound( expected_product / 5.0 ); + eosio::check( std::abs(1.0 / itr->sec - 4000000.0) <= tolerance, "idx_double_general - upper_bound" ); + + } + } + + [[eosio::action("sldg")]] void idx_long_double_general() { + using namespace _test_multi_index; + + typedef record_idx_long_double record; + + auto payer = get_self(); + + eosio::print("Testing long double secondary index.\n"); + eosio::multi_index<"floattable2"_n, record, + eosio::indexed_by<"bysecondary"_n, eosio::const_mem_fun> + > table( get_self(), get_self().value ); + + auto secidx = table.get_index<"bysecondary"_n>(); + + long double tolerance = std::min( static_cast(std::numeric_limits::epsilon()), + std::numeric_limits::epsilon() * 1e7l ); + eosio::print("tolerance = ", tolerance, "\n"); + + long double f = 1.0l; + for( uint64_t i = 1; i <= 10; ++i, f += 1.0l ) { + table.emplace( payer, [&](auto& o) { + o.id = i; + o.sec = 1.0l / (i * 1000000.0l); + }); + } + + long double expected_product = 1.0l / 1000000.0l; + eosio::print( "expected_product = ", expected_product, "\n" ); + + uint64_t expected_key = 10; + for( const auto& obj : secidx ) { + eosio::check( obj.primary_key() == expected_key, "idx_long_double_general - unexpected primary key" ); + + long double prod = obj.sec * obj.id; + + eosio::print(" id = ", obj.id, ", sec = ", obj.sec, ", sec * id = ", prod, "\n"); + + eosio::check( std::abs(prod - expected_product) <= tolerance, + "idx_long_double_general - product of secondary and id not equal to expected_product within tolerance" ); + + --expected_key; + } + eosio::check( expected_key == 0, "idx_long_double_general - did not iterate through secondary index properly" ); + + { + auto itr = secidx.lower_bound( expected_product / 5.5l ); + eosio::check( std::abs(1.0l / itr->sec - 5000000.0l) <= tolerance, "idx_long_double_general - lower_bound" ); + + itr = secidx.upper_bound( expected_product / 5.0l ); + eosio::check( std::abs(1.0l / itr->sec - 4000000.0l) <= tolerance, "idx_long_double_general - upper_bound" ); + + } + } +};