diff --git a/cmake/TestsExternalProject.cmake b/cmake/TestsExternalProject.cmake index 5e9ae7469..1bce537be 100644 --- a/cmake/TestsExternalProject.cmake +++ b/cmake/TestsExternalProject.cmake @@ -43,7 +43,7 @@ ExternalProject_Add( ) if(ENABLE_INTEGRATION_TESTS) - message(STATUS "Building integration tests as BUILD_INTEGRATION_TESTS is ON") + message(STATUS "Building integration tests as ENABLE_INTEGRATION_TESTS is ON") find_package(sysio REQUIRED) @@ -55,20 +55,33 @@ if(ENABLE_INTEGRATION_TESTS) string(REPLACE ";" "|" TEST_FRAMEWORK_PATH "${CMAKE_FRAMEWORK_PATH}") string(REPLACE ";" "|" TEST_MODULE_PATH "${CMAKE_MODULE_PATH}") + string(REPLACE ";" "|" TEST_PREFIX_PATH "${CMAKE_PREFIX_PATH}") ExternalProject_Add( CDTIntegrationTests SOURCE_DIR "${CMAKE_SOURCE_DIR}/tests/integration" BINARY_DIR "${CMAKE_BINARY_DIR}/tests/integration" - CMAKE_ARGS -DCMAKE_BUILD_TYPE=${TEST_BUILD_TYPE} -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} -DCMAKE_FRAMEWORK_PATH=${TEST_FRAMEWORK_PATH} -DCMAKE_MODULE_PATH="${TEST_MODULE_PATH};${CMAKE_MODULE_PATH}" -DCMAKE_PREFIX_PATH="${CMAKE_PREFIX_PATH}" -Dsysio_DIR=${sysio_DIR} -DLLVM_DIR=${LLVM_DIR} -DBOOST_ROOT=${BOOST_ROOT} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + LIST_SEPARATOR | + CMAKE_ARGS + -DCMAKE_BUILD_TYPE=${TEST_BUILD_TYPE} + -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} + -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + -DCMAKE_FRAMEWORK_PATH=${TEST_FRAMEWORK_PATH} + -DCMAKE_MODULE_PATH=${TEST_MODULE_PATH} + -DCMAKE_PREFIX_PATH=${TEST_PREFIX_PATH} + -Dsysio_DIR=${sysio_DIR} + -DLLVM_DIR=${LLVM_DIR} + -DBOOST_ROOT=${BOOST_ROOT} + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON UPDATE_COMMAND "" PATCH_COMMAND "" TEST_COMMAND "" INSTALL_COMMAND "" BUILD_ALWAYS 1 + DEPENDS CDTWasmTests ) else() - message(STATUS "Skipping building integration tests as BUILD_INTEGRATION_TESTS is OFF") + message(STATUS "Skipping building integration tests as ENABLE_INTEGRATION_TESTS is OFF") return() endif() \ No newline at end of file diff --git a/cmake/ToolsExternalProject.cmake b/cmake/ToolsExternalProject.cmake index dd2335401..7beabf74b 100644 --- a/cmake/ToolsExternalProject.cmake +++ b/cmake/ToolsExternalProject.cmake @@ -22,10 +22,11 @@ ExternalProject_Add( -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} - -DCMAKE_PREFIX_PATH=${_vcpkg_share}/.. -DCMAKE_INSTALL_BINDIR=${CMAKE_INSTALL_BINDIR} + -DCMAKE_PREFIX_PATH=${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - -DVCPKG_INSTALLED_DIR=${CMAKE_BINARY_DIR}/vcpkg_installed + -DVCPKG_INSTALLED_DIR=${VCPKG_INSTALLED_DIR} + -DVCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET} -DCDT_INSTALL_PREFIX=${CDT_INSTALL_PREFIX} -DLLVM_DIR=${LLVM_DIR} -DClang_DIR=${Clang_DIR} @@ -33,6 +34,7 @@ ExternalProject_Add( -DVERSION_MAJOR=${VERSION_MAJOR} -DVERSION_MINOR=${VERSION_MINOR} -DVERSION_PATCH=${VERSION_PATCH} + SOURCE_DIR "${CMAKE_SOURCE_DIR}/tools" BINARY_DIR "${CMAKE_BINARY_DIR}/tools" UPDATE_COMMAND "" diff --git a/docs/kv-abi-key-metadata.md b/docs/kv-abi-key-metadata.md new file mode 100644 index 000000000..363e8a5ff --- /dev/null +++ b/docs/kv-abi-key-metadata.md @@ -0,0 +1,116 @@ +# KV Table Key Metadata in ABI + +## Overview + +The generated ABI includes key layout metadata in the `key_names` and `key_types` fields of each table entry. This allows SHiP clients (e.g., Hyperion) to decode raw KV key bytes using the contract's ABI. + +Key metadata is generated in two ways: + +1. **Automatically** for tables used in `kv_multi_index`, `multi_index`, `singleton`, or `kv::table` templates — standard 24-byte key layout. +2. **Explicitly** via `[[sysio::kv_key("key_struct")]]` for tables backed by `kv::raw_table` — custom key layout from the named struct's fields. + +## Automatic Key Metadata (kv\_multi\_index) + +For tables used in a `kv_multi_index` (or `multi_index`) template: + +```cpp +struct [[sysio::table]] account { + asset balance; + uint64_t primary_key() const { return balance.symbol.code().raw(); } +}; +typedef kv_multi_index<"accounts"_n, account> accounts_table; +``` + +CDT generates the standard 24-byte key layout: + +```json +{ + "name": "accounts", + "type": "account", + "key_names": ["table_name", "scope", "primary_key"], + "key_types": ["name", "name", "uint64"] +} +``` + +## Custom Key Metadata (\[\[sysio::kv\_key\]\]) + +For tables backed by `kv::raw_table` with custom key types, annotate the value struct: + +```cpp +struct my_key { + std::string region; + uint64_t id; + SYSLIB_SERIALIZE(my_key, (region)(id)) +}; + +struct [[sysio::table("geodata"), sysio::kv_key("my_key")]] my_value { + std::string payload; + uint64_t amount; + SYSLIB_SERIALIZE(my_value, (payload)(amount)) +}; + +kv::raw_table geodata; +``` + +CDT generates key metadata from the named struct's fields and adds the key struct to the ABI: + +```json +{ + "name": "geodata", + "type": "my_value", + "key_names": ["region", "id"], + "key_types": ["string", "uint64"] +} +``` + +Using `[[sysio::kv_key]]` without an argument produces the standard 24-byte layout (same as automatic). + +## Key Encoding + +All KV keys use big-endian encoding for correct `memcmp`-based ordering. + +### Standard layout (kv\_multi\_index) + +| Offset | Size | Field | Type | +|--------|------|-------|------| +| 0 | 8 bytes | `table_name` | `name` (BE uint64) | +| 8 | 8 bytes | `scope` | `name` (BE uint64) | +| 16 | 8 bytes | `primary_key` | `uint64` (BE) | + +### Custom layout (kv::raw_table) + +Fields are concatenated in `SYSLIB_SERIALIZE` declaration order: + +| Type | Encoding | +|------|----------| +| `uint8` | 1 byte | +| `uint16` | 2 bytes BE | +| `uint32` | 4 bytes BE | +| `uint64` / `name` | 8 bytes BE | +| `uint128` | 16 bytes BE | +| `double` | 8 bytes (IEEE 754 with sign-flip for sort order) | +| `string` | Raw bytes + `0x00` terminator (no embedded nulls) | +| `bool` | 1 byte (0 or 1) | + +## How SHiP Clients Use This + +### Format=1 (`contract_row` delta) + +SHiP pre-decomposes the 24-byte key into named fields (`table`, `scope`, `primary_key`). Clients receive these directly and decode the `value` using the table's `type` field from the ABI. Key metadata is informational. + +### Format=0 (`contract_row_kv` delta) + +SHiP sends `{code, payer, key, value}` with the raw key as opaque bytes. Clients: + +1. Load the contract ABI +2. Find the table entry with populated `key_names`/`key_types` +3. Decode key fields as big-endian per the declared types +4. Decode the value using `abieos` with the table's `type` field + +## Value Encoding + +Values use standard ABI serialization (little-endian) for both format=0 and format=1. Clients decode them using `abieos` with the struct type from the table's `type` field. + +## Backward Compatibility + +The `key_names` and `key_types` fields have always been present in the ABI schema but were previously empty arrays. Populating them is backward-compatible — existing parsers that ignore these fields are unaffected. diff --git a/docs/kv-intrinsics-reference.md b/docs/kv-intrinsics-reference.md new file mode 100644 index 000000000..67f289da5 --- /dev/null +++ b/docs/kv-intrinsics-reference.md @@ -0,0 +1,667 @@ +# Wire KV Intrinsics Reference + +This document describes the 22 host functions (intrinsics) that implement the Wire KV database. These are declared in `` and implemented by the `nodeop` runtime. + +Contract developers typically use the higher-level `sysio::multi_index` or `sysio::kv::table` APIs rather than calling these intrinsics directly. This reference is intended for CDT library authors, tooling developers, and anyone who needs to understand the low-level interface. + +--- + +## General Notes + +### key\_format parameter + +Several intrinsics accept a `key_format` parameter: + +| Value | Meaning | +|-------|---------| +| 0 | Raw key -- arbitrary bytes, no special encoding assumed | +| 1 | Standard 24-byte key -- `[table:8B BE][scope:8B BE][pk:8B BE]`. Enables SSO fast-path and SHiP translation | + +The `multi_index` and `kv::table` APIs pass `key_format=1` (standard 24-byte keys). The `kv::raw_table` API passes `key_format=0` (raw keys with variable-length encoding). + +### payer parameter + +The `payer` parameter on `kv_set`: + +| Value | Meaning | +|-------|---------| +| 0 | RAM is charged to the executing contract account (normal case) | +| nonzero | RAM is charged to the specified account. Payer must have authorized the action or net RAM usage must not increase | + +### Limits + +| Resource | Limit | +|----------|-------| +| Maximum key size | 256 bytes | +| Maximum value size | 256 KiB (262,144 bytes) | +| Simultaneous primary iterator handles | 16 per action | +| Simultaneous secondary iterator handles | 16 per action | +| Maximum secondary key size | 256 bytes | +| Maximum primary key size (secondary index) | 256 bytes | + +### Iterator status codes + +Primary iterators (`kv_it_*`) and secondary iterators (`kv_idx_*`) return status codes: + +| Code | Meaning | +|------|---------| +| 0 | OK -- iterator points to a valid entry | +| 1 | End -- iterator is past the last entry in the prefix/index range | +| 2 | Erased -- the entry the iterator pointed to was deleted | + +--- + +## 1. Primary Operations + +### kv\_set + +Store or update a key-value pair. + +```c +int64_t kv_set(uint32_t key_format, uint64_t payer, + const void* key, uint32_t key_size, + const void* value, uint32_t value_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `key_format` | `uint32_t` | Key encoding format (0=raw, 1=standard 24-byte) | +| `payer` | `uint64_t` | RAM payer account (0=self) | +| `key` | `const void*` | Pointer to key bytes | +| `key_size` | `uint32_t` | Length of key in bytes (max 256) | +| `value` | `const void*` | Pointer to value bytes | +| `value_size` | `uint32_t` | Length of value in bytes (max 256 KiB) | + +**Returns:** `int64_t` -- RAM byte delta. Positive for growth (new key or larger value), zero for same-size update, negative if value shrunk. + +**Behavior:** +- If the key does not exist, creates a new entry +- If the key exists, replaces the entire value +- RAM usage delta is applied to the payer account + +**Error conditions:** +- `key_size > 256`: aborts with "key too large" +- `value_size > 262144`: aborts with "value too large" +- Nonzero `payer` without payer authorization: aborts with `unauthorized_ram_usage_increase` if net RAM increases + +--- + +### kv\_get + +Read a value by key. + +```c +int32_t kv_get(uint32_t key_format, uint64_t code, + const void* key, uint32_t key_size, + void* value, uint32_t value_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `key_format` | `uint32_t` | 0 for raw keys (kv::raw\_table), 1 for standard 24-byte keys (multi\_index / kv::table). Determines which index partition to query. | +| `code` | `uint64_t` | Contract account to read from (allows cross-contract reads) | +| `key` | `const void*` | Pointer to key bytes | +| `key_size` | `uint32_t` | Length of key | +| `value` | `void*` | Output buffer for the value | +| `value_size` | `uint32_t` | Size of the output buffer | + +**Returns:** `int32_t` -- Actual value size in bytes, or `-1` if the key was not found. + +**Behavior:** +- Reads the value associated with `key` from the specified contract's storage +- If `value_size` is smaller than the actual value, only `value_size` bytes are written to the buffer. The return value still reports the full size, allowing the caller to allocate a larger buffer and retry. +- Passing `value=nullptr, value_size=0` is valid and returns the size without copying data (size probe) +- Cross-contract reads are allowed: any contract can read another contract's KV data by specifying its account as `code` + +**Error conditions:** +- None (returns -1 for missing keys rather than aborting) + +--- + +### kv\_erase + +Delete a key-value pair. + +```c +int64_t kv_erase(uint32_t key_format, const void* key, uint32_t key_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `key_format` | `uint32_t` | 0 for raw keys (kv::raw\_table), 1 for standard 24-byte keys (multi\_index / kv::table). Determines which index partition to query. | +| `key` | `const void*` | Pointer to key bytes | +| `key_size` | `uint32_t` | Length of key | + +**Returns:** `int64_t` -- Negative RAM byte delta (bytes freed). + +**Behavior:** +- Removes the key-value pair from the executing contract's storage +- Any iterators pointing to the erased key become invalid (status = 2/erased) + +**Error conditions:** +- Key not found: aborts with "key not found" +- Cannot erase keys belonging to other contracts + +--- + +### kv\_contains + +Check if a key exists. + +```c +int32_t kv_contains(uint32_t key_format, uint64_t code, + const void* key, uint32_t key_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `key_format` | `uint32_t` | 0 for raw keys (kv::raw\_table), 1 for standard 24-byte keys (multi\_index / kv::table). Determines which index partition to query. | +| `code` | `uint64_t` | Contract account to check | +| `key` | `const void*` | Pointer to key bytes | +| `key_size` | `uint32_t` | Length of key | + +**Returns:** `int32_t` -- `1` if the key exists, `0` otherwise. + +**Behavior:** +- Lightweight existence check without reading the value +- Cross-contract reads allowed (same as `kv_get`) + +**Error conditions:** +- None + +--- + +## 2. Primary Iterators + +Primary iterators traverse KV entries matching a key prefix. They are used to implement table scans, range queries, and ordered iteration. + +### kv\_it\_create + +Create a new iterator over keys matching a prefix. + +```c +uint32_t kv_it_create(uint32_t key_format, uint64_t code, + const void* prefix, uint32_t prefix_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `key_format` | `uint32_t` | 0 for raw keys (kv::raw\_table), 1 for standard 24-byte keys (multi\_index / kv::table). Determines which index partition to query. | +| `code` | `uint64_t` | Contract account to iterate over | +| `prefix` | `const void*` | Key prefix bytes. Empty prefix (`prefix_size=0`) iterates all keys | +| `prefix_size` | `uint32_t` | Length of prefix | + +**Returns:** `uint32_t` -- Iterator handle (0..15). + +**Behavior:** +- Creates a prefix-scoped iterator positioned at the first key that starts with `prefix` +- If no keys match the prefix, the iterator starts at end (status = 1) +- The CDT APIs use a 16-byte prefix (`[table:8B][scope:8B]`) to scope iteration to a single table+scope + +**Error conditions:** +- More than 16 simultaneous iterators: aborts with "too many iterators" + +--- + +### kv\_it\_destroy + +Destroy an iterator and free its slot. + +```c +void kv_it_destroy(uint32_t handle); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `handle` | `uint32_t` | Iterator handle to destroy | + +**Behavior:** +- Frees the iterator slot for reuse +- Must be called for every created iterator to avoid exhausting the pool + +**Error conditions:** +- Invalid handle: aborts + +--- + +### kv\_it\_status + +Query the current status of an iterator. + +```c +int32_t kv_it_status(uint32_t handle); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `handle` | `uint32_t` | Iterator handle | + +**Returns:** `int32_t` -- Status code (0=OK, 1=end, 2=erased). + +--- + +### kv\_it\_next + +Advance the iterator to the next key within the prefix range. + +```c +int32_t kv_it_next(uint32_t handle); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `handle` | `uint32_t` | Iterator handle | + +**Returns:** `int32_t` -- Status after advancing (0=OK, 1=end). + +**Behavior:** +- Moves to the next key in lexicographic order that still matches the prefix +- If already at the last matching key, moves to end (returns 1) + +--- + +### kv\_it\_prev + +Move the iterator to the previous key within the prefix range. + +```c +int32_t kv_it_prev(uint32_t handle); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `handle` | `uint32_t` | Iterator handle | + +**Returns:** `int32_t` -- Status after retreating (0=OK, 1=end). + +**Behavior:** +- Moves to the previous key in lexicographic order that still matches the prefix +- If at the first matching key, returns 1 (end/before-begin) +- If at end, moves to the last matching key + +--- + +### kv\_it\_lower\_bound + +Seek the iterator to the lower bound of a key within the prefix range. + +```c +int32_t kv_it_lower_bound(uint32_t handle, const void* key, uint32_t key_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `handle` | `uint32_t` | Iterator handle | +| `key` | `const void*` | Seek key bytes | +| `key_size` | `uint32_t` | Length of seek key | + +**Returns:** `int32_t` -- Status after seeking (0=OK, 1=end). + +**Behavior:** +- Positions the iterator at the first key >= `key` that matches the iterator's prefix +- If no such key exists, positions at end (returns 1) + +--- + +### kv\_it\_key + +Read the current key from the iterator. + +```c +int32_t kv_it_key(uint32_t handle, uint32_t offset, + void* dest, uint32_t dest_size, + uint32_t* actual_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `handle` | `uint32_t` | Iterator handle | +| `offset` | `uint32_t` | Byte offset into the key (for partial reads) | +| `dest` | `void*` | Output buffer | +| `dest_size` | `uint32_t` | Size of output buffer | +| `actual_size` | `uint32_t*` | [out] Actual total key size | + +**Returns:** `int32_t` -- Status code (0=OK, nonzero=invalid position). + +**Behavior:** +- Copies up to `dest_size` bytes of the key (starting from `offset`) into `dest` +- Writes the total key size to `*actual_size` regardless of how many bytes were copied +- The `offset` parameter enables reading large keys in chunks + +--- + +### kv\_it\_value + +Read the current value from the iterator. + +```c +int32_t kv_it_value(uint32_t handle, uint32_t offset, + void* dest, uint32_t dest_size, + uint32_t* actual_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `handle` | `uint32_t` | Iterator handle | +| `offset` | `uint32_t` | Byte offset into the value | +| `dest` | `void*` | Output buffer | +| `dest_size` | `uint32_t` | Size of output buffer | +| `actual_size` | `uint32_t*` | [out] Actual total value size | + +**Returns:** `int32_t` -- Status code (0=OK, nonzero=invalid position). + +**Behavior:** +- Same semantics as `kv_it_key` but reads the value instead of the key + +--- + +## 3. Secondary Index Operations + +Secondary indices map secondary keys to primary keys. They are used by `sysio::multi_index` to implement `indexed_by` / `get_index<>()`. Each secondary index is identified by a `(table, index_id)` pair. + +Secondary iterators use a separate handle pool from primary iterators (16 handles each). + +### kv\_idx\_store + +Store a secondary index entry. + +```c +void kv_idx_store(uint64_t payer, uint64_t table, uint32_t index_id, + const void* pri_key, uint32_t pri_key_size, + const void* sec_key, uint32_t sec_key_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `payer` | `uint64_t` | Account to bill for RAM (0 = receiver) | +| `table` | `uint64_t` | Logical table name (e.g., `"accounts"_n.value`) | +| `index_id` | `uint32_t` | Index identifier (0-255, corresponding to the Nth `indexed_by`) | +| `pri_key` | `const void*` | Primary key bytes (`[scope:8B][pk:8B]` = 16 bytes in CDT) | +| `pri_key_size` | `uint32_t` | Primary key size (max 256) | +| `sec_key` | `const void*` | Secondary key bytes (big-endian encoded) | +| `sec_key_size` | `uint32_t` | Secondary key size (max 256) | + +**Behavior:** +- Inserts a (secondary\_key, primary\_key) pair into the index +- Multiple entries with the same secondary key are allowed (sorted by primary key) +- RAM is charged to the specified payer (same account as the primary row's payer) +- The payer is stored on the index entry for correct refunds on removal + +**Error conditions:** +- Duplicate (sec\_key, pri\_key) pair: aborts + +--- + +### kv\_idx\_remove + +Remove a secondary index entry. + +```c +void kv_idx_remove(uint64_t table, uint32_t index_id, + const void* pri_key, uint32_t pri_key_size, + const void* sec_key, uint32_t sec_key_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `table` | `uint64_t` | Logical table name | +| `index_id` | `uint32_t` | Index identifier | +| `pri_key` | `const void*` | Primary key bytes | +| `pri_key_size` | `uint32_t` | Primary key size | +| `sec_key` | `const void*` | Secondary key bytes | +| `sec_key_size` | `uint32_t` | Secondary key size | + +**Behavior:** +- Removes the exact (sec\_key, pri\_key) pair from the index +- RAM is refunded to the stored payer (the account that was billed on `kv_idx_store`) +- Any iterators pointing to the removed entry become invalid + +**Error conditions:** +- Entry not found: aborts + +--- + +### kv\_idx\_update + +Update a secondary index entry (change the secondary key, keep the primary key). + +```c +void kv_idx_update(uint64_t payer, uint64_t table, uint32_t index_id, + const void* pri_key, uint32_t pri_key_size, + const void* old_sec_key, uint32_t old_sec_key_size, + const void* new_sec_key, uint32_t new_sec_key_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `payer` | `uint64_t` | Account to bill for RAM (0 = receiver) | +| `table` | `uint64_t` | Logical table name | +| `index_id` | `uint32_t` | Index identifier | +| `pri_key` | `const void*` | Primary key bytes (unchanged) | +| `pri_key_size` | `uint32_t` | Primary key size | +| `old_sec_key` | `const void*` | Current secondary key bytes | +| `old_sec_key_size` | `uint32_t` | Current secondary key size | +| `new_sec_key` | `const void*` | New secondary key bytes | +| `new_sec_key_size` | `uint32_t` | New secondary key size | + +**Behavior:** +- Atomically removes the old (old\_sec\_key, pri\_key) entry and inserts (new\_sec\_key, pri\_key) +- If payer changed from stored payer: refunds old payer full amount, charges new payer full amount +- If same payer: charges/refunds only the size delta +- If old\_sec\_key == new\_sec\_key, this is a no-op (called by CDT, skipped at CDT layer) +- More efficient than separate remove + store calls + +**Error conditions:** +- Old entry not found: aborts + +--- + +### kv\_idx\_find\_secondary + +Find an exact secondary key match. Returns an iterator handle. + +```c +uint32_t kv_idx_find_secondary(uint64_t code, uint64_t table, uint32_t index_id, + const void* sec_key, uint32_t sec_key_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `code` | `uint64_t` | Contract account to search | +| `table` | `uint64_t` | Logical table name | +| `index_id` | `uint32_t` | Index identifier | +| `sec_key` | `const void*` | Secondary key bytes to find | +| `sec_key_size` | `uint32_t` | Secondary key size | + +**Returns:** `uint32_t` -- Secondary iterator handle. Check status to determine if a match was found. + +**Behavior:** +- Positions the iterator at the first entry with an exact match on `sec_key` +- If no match, iterator is at end (status = 1) +- Cross-contract reads allowed + +--- + +### kv\_idx\_lower\_bound + +Find the lower bound on a secondary key. + +```c +uint32_t kv_idx_lower_bound(uint64_t code, uint64_t table, uint32_t index_id, + const void* sec_key, uint32_t sec_key_size); +``` + +**Parameters:** Same as `kv_idx_find_secondary`. + +**Returns:** `uint32_t` -- Secondary iterator handle positioned at the first entry with sec\_key >= the given key. + +**Behavior:** +- Positions at the first entry whose secondary key is >= `sec_key` +- If no such entry exists, positioned at end +- Passing `sec_key=nullptr, sec_key_size=0` positions at the very first entry in the index + +--- + +### kv\_idx\_next + +Advance the secondary iterator to the next entry. + +```c +int32_t kv_idx_next(uint32_t handle); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `handle` | `uint32_t` | Secondary iterator handle | + +**Returns:** `int32_t` -- Status after advancing (0=OK, 1=end). + +**Behavior:** +- Moves to the next entry in secondary key order +- Entries with the same secondary key are ordered by primary key + +--- + +### kv\_idx\_prev + +Move the secondary iterator to the previous entry. + +```c +int32_t kv_idx_prev(uint32_t handle); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `handle` | `uint32_t` | Secondary iterator handle | + +**Returns:** `int32_t` -- Status after retreating (0=OK, 1=end). + +--- + +### kv\_idx\_key + +Read the current secondary key from the iterator. + +```c +int32_t kv_idx_key(uint32_t handle, uint32_t offset, + void* dest, uint32_t dest_size, + uint32_t* actual_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `handle` | `uint32_t` | Secondary iterator handle | +| `offset` | `uint32_t` | Byte offset into the secondary key | +| `dest` | `void*` | Output buffer | +| `dest_size` | `uint32_t` | Size of output buffer | +| `actual_size` | `uint32_t*` | [out] Actual total secondary key size | + +**Returns:** `int32_t` -- Status code (0=OK). + +--- + +### kv\_idx\_primary\_key + +Read the primary key associated with the current secondary index entry. + +```c +int32_t kv_idx_primary_key(uint32_t handle, uint32_t offset, + void* dest, uint32_t dest_size, + uint32_t* actual_size); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `handle` | `uint32_t` | Secondary iterator handle | +| `offset` | `uint32_t` | Byte offset into the primary key | +| `dest` | `void*` | Output buffer | +| `dest_size` | `uint32_t` | Size of output buffer | +| `actual_size` | `uint32_t*` | [out] Actual total primary key size | + +**Returns:** `int32_t` -- Status code (0=OK). + +**Behavior:** +- In the CDT multi\_index implementation, the stored primary key is `[scope:8B][pk:8B]` = 16 bytes +- The caller extracts the 8-byte primary key from offset 8 + +--- + +### kv\_idx\_destroy + +Destroy a secondary iterator and free its slot. + +```c +void kv_idx_destroy(uint32_t handle); +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `handle` | `uint32_t` | Secondary iterator handle to destroy | + +**Behavior:** +- Frees the secondary iterator slot for reuse +- Must be called for every created secondary iterator + +--- + +## Source Files + +| File | Description | +|------|-------------| +| `libraries/sysiolib/capi/sysio/kv.h` | C API declarations (CDT side) | +| `libraries/sysiolib/contracts/sysio/kv_table.hpp` | `sysio::kv::table` implementation | +| `libraries/sysiolib/contracts/sysio/kv_multi_index.hpp` | `sysio::multi_index` (KV-backed) implementation | +| `libraries/sysiolib/contracts/sysio/kv_singleton.hpp` | `sysio::singleton` (KV-backed) implementation | +| `libraries/sysiolib/contracts/sysio/singleton.hpp` | Backward-compat alias to kv\_singleton | +| `libraries/sysiolib/contracts/sysio/multi_index.hpp` | Backward-compat alias to kv\_multi\_index | + +On the node side (wire-sysio repository): + +| File | Description | +|------|-------------| +| `libraries/chain/include/sysio/chain/webassembly/interface.hpp` | Host function declarations | +| `libraries/chain/apply_context.cpp` | KV intrinsic implementations | +| `libraries/chain/include/sysio/chain/kv_table_objects.hpp` | KV storage objects (chainbase) | +| `libraries/chain/webassembly/kv_database.cpp` | WASM-to-native bridge | diff --git a/docs/kv-storage-guide.md b/docs/kv-storage-guide.md new file mode 100644 index 000000000..01ea6fe08 --- /dev/null +++ b/docs/kv-storage-guide.md @@ -0,0 +1,479 @@ +# Wire KV Storage Guide + +Wire uses a key-value database as the storage backend for smart contract state, replacing the EOSIO `db_*_i64` host functions with a simpler set of KV intrinsics. Two C++ APIs are available to contract developers: + +- **`sysio::multi_index`** -- Full-featured table with secondary indices, reverse iteration, object caching, and singletons. Recommended for most contracts. API-compatible with EOSIO `multi_index`. +- **`sysio::kv::table`** -- High-performance, zero-copy table for simple CRUD workloads. No secondary index support, but ~15% faster for trivially copyable structs. + +Both APIs use the same underlying KV host functions (intrinsics). Existing contracts compile unchanged with the new CDT -- no source code modifications required. + +--- + +## sysio::multi\_index (recommended for most contracts) + +### Include + +```cpp +#include // explicit +// or automatically via: +#include +``` + +### Overview + +The API surface is identical to the EOSIO `multi_index`. Under the hood, primary rows are stored as 24-byte KV keys and secondary indices use the `kv_idx_*` intrinsics, but from the contract author's perspective the interface is the same. + +Key properties: + +- Up to **16 secondary indices** via `indexed_by` / `const_mem_fun` +- Full bidirectional iterator support: `begin`/`end`, `rbegin`/`rend`, `cbegin`/`cend` +- `find`, `require_find`, `get`, `lower_bound`, `upper_bound`, `iterator_to` +- `emplace`, `modify`, `erase` (erase returns next iterator) +- `available_primary_key()` for auto-increment +- Object caching: repeated access to the same primary key returns the cached pointer +- `payer` parameter is accepted for API compatibility but ignored -- RAM is charged to the contract account +- Row types must use `SYSLIB_SERIALIZE` + +### Singleton support + +```cpp +#include +``` + +`sysio::singleton` is now an alias for `sysio::kv_singleton`, backed by the KV database. The API is unchanged: + +| Method | Description | +|--------|-------------| +| `exists()` | Returns true if a value has been stored | +| `get()` | Returns stored value, asserts if missing | +| `get_or_default(def)` | Returns stored value or `def` | +| `get_or_create(payer, def)` | Returns stored value, or stores and returns `def` | +| `set(value, payer)` | Stores or updates the value | +| `remove()` | Deletes the stored value | + +### Secondary index views + +Access a secondary index with `get_index<"indexname"_n>()`. The returned view supports: + +| Method | Description | +|--------|-------------| +| `find(sec_key)` | Exact match on secondary key | +| `lower_bound(sec_key)` | First entry >= sec\_key | +| `require_find(sec_key, msg)` | find() + assert | +| `begin()` / `end()` | Full range in secondary key order | +| `rbegin()` / `rend()` | Reverse iteration | +| `modify(itr, payer, updater)` | Modify the primary row via secondary iterator | +| `erase(itr)` | Erase the primary row, returns next secondary iterator | + +Supported secondary key types: `uint64_t`, `uint128_t`, `double`, `long double`, and any serializable type. + +### Example: token-like contract with secondary index + +```cpp +#include + +using namespace sysio; + +class [[sysio::contract]] mytoken : public contract { +public: + using contract::contract; + + struct [[sysio::table]] account { + name owner; + uint64_t balance; + + uint64_t primary_key() const { return owner.value; } + uint64_t by_balance() const { return balance; } + + SYSLIB_SERIALIZE(account, (owner)(balance)) + }; + + using accounts_table = multi_index<"accounts"_n, account, + indexed_by<"bybalance"_n, const_mem_fun> + >; + + [[sysio::action]] + void transfer(name from, name to, uint64_t amount) { + require_auth(from); + + accounts_table accts(get_self(), get_self().value); + + // Debit sender + auto from_itr = accts.require_find(from.value, "sender not found"); + check(from_itr->balance >= amount, "insufficient balance"); + accts.modify(from_itr, same_payer, [&](auto& a) { + a.balance -= amount; + }); + + // Credit receiver + auto to_itr = accts.find(to.value); + if (to_itr == accts.end()) { + accts.emplace(get_self(), [&](auto& a) { + a.owner = to; + a.balance = amount; + }); + } else { + accts.modify(to_itr, same_payer, [&](auto& a) { + a.balance += amount; + }); + } + } + + [[sysio::action]] + void topbalances(uint32_t limit) { + accounts_table accts(get_self(), get_self().value); + auto idx = accts.get_index<"bybalance"_n>(); + + // Iterate in reverse (highest balance first) + uint32_t count = 0; + for (auto it = idx.rbegin(); it != idx.rend() && count < limit; ++it, ++count) { + print(it->owner, ": ", it->balance, "\n"); + } + } +}; +``` + +--- + +## sysio::kv::table (for performance-critical contracts) + +### Include + +```cpp +#include +``` + +### Overview + +`sysio::kv::table` provides a simpler, lower-overhead interface for contracts that do not need secondary indices. Key properties: + +- **Zero-copy** for `trivially_copyable` structs: values are stored/loaded via `memcpy` instead of datastream serialization, provided `sizeof(T) == pack_size(T)` (no struct padding) +- Falls back to datastream serialization for complex types (vectors, strings, nested structs) +- No secondary index support +- No object caching -- each `find`/`get` call reads from storage +- Bidirectional iterators: `begin`/`end`, `lower_bound`/`upper_bound` +- Cross-scope iteration via `begin_all_scopes()` / `end_all_scopes()` (not possible with multi\_index) +- Row type must have a `uint64_t primary_key() const` method + +### API reference + +| Method | Description | +|--------|-------------| +| `find(pk)` | Returns iterator, or `end()` if not found | +| `require_find(pk, msg)` | find() + assert | +| `get(pk, msg)` | Returns `const T&`, asserts if missing | +| `contains(pk)` | Returns `bool`, single intrinsic call | +| `lower_bound(pk)` | First entry with key >= pk | +| `upper_bound(pk)` | First entry with key > pk | +| `begin()` / `end()` | Forward iteration over all rows in scope | +| `set(pk, obj)` | Store directly by primary key | +| `emplace(payer, constructor)` | Construct and store a new row | +| `modify(itr, payer, updater)` | Update an existing row | +| `modify(obj, payer, updater)` | Update via object reference | +| `erase(pk)` / `erase(obj)` / `erase(itr)` | Delete a row | +| `available_primary_key()` | Next auto-increment key | +| `begin_all_scopes()` | Iterate ALL rows across ALL scopes (returns `scoped_row` with scope, primary\_key, obj) | + +### Example: high-performance account balance table + +```cpp +#include +#include + +using namespace sysio; + +struct balance_row { + uint64_t account; // name as uint64_t + uint64_t amount; + + uint64_t primary_key() const { return account; } + + // trivially_copyable + no padding = zero-copy path + SYSLIB_SERIALIZE(balance_row, (account)(amount)) +}; + +using balance_table = kv::table<"balances"_n, balance_row>; + +class [[sysio::contract]] fasttoken : public contract { +public: + using contract::contract; + + [[sysio::action]] + void transfer(name from, name to, uint64_t amount) { + require_auth(from); + balance_table bal(get_self(), get_self().value); + + // Debit + auto sender = bal.get(from.value, "sender not found"); + check(sender.amount >= amount, "insufficient balance"); + balance_row updated_sender = sender; + updated_sender.amount -= amount; + bal.set(from.value, updated_sender); + + // Credit + balance_row receiver; + if (bal.contains(to.value)) { + receiver = bal.get(to.value); + receiver.amount += amount; + } else { + receiver.account = to.value; + receiver.amount = amount; + } + bal.set(to.value, receiver); + } +}; +``` + +--- + +## Key Encoding + +Both APIs encode primary keys as 24 bytes: + +``` +[table_name: 8 bytes, big-endian] [scope: 8 bytes, big-endian] [primary_key: 8 bytes, big-endian] +``` + +This encoding has several desirable properties: + +- **SSO (Small String Optimization)**: keys <= 24 bytes are stored inline in the `kv_object` without heap allocation +- **Integer fast-path**: 8-byte big-endian keys can be compared with a single `bswap64` instruction +- **Lexicographic ordering**: big-endian encoding preserves numeric sort order in byte comparison, enabling prefix-scoped iteration +- **SHiP compatible**: keys can be reverse-mapped to the legacy `contract_row` format (table, scope, primary\_key) for State History Plugin consumers + +Secondary index keys are encoded using `kv_idx_*` intrinsics with a composite primary key of `[scope:8B][pk:8B]` to ensure uniqueness across scopes. + +--- + +## Performance Comparison + +| Scenario | multi\_index | kv::table (zero-copy) | kv::table (complex) | +|----------|-------------|----------------------|---------------------| +| Simple row read | Baseline | ~15% faster | ~same | +| Simple row write | Baseline | ~15% faster | ~same | +| With secondary indices | Supported | N/A | N/A | +| OC runtime (token transfer) | ~0.5 us | ~0.5 us | ~0.5 us | +| Serialization overhead | Always serializes | memcpy for POD | Serializes | + +The zero-copy path in `kv::table` skips datastream serialization entirely for structs where `std::is_trivially_copyable` is true and `sizeof(T) == pack_size(T)` (no padding bytes). For complex types with vectors, strings, or optional fields, both APIs have equivalent performance since both use datastream serialization. + +At the OC (Optimized Compiler) runtime tier, intrinsic call overhead dominates and both APIs perform at near-parity with the EOSIO `db_*_i64` implementation. + +--- + +## Performance Best Practices + +### What's Fast + +- **Point lookups by primary key** (`find(pk)`, `get(pk)`) -- single KV read, O(1) with SSO key +- **kv::table with trivially\_copyable structs** -- zero-copy (memcpy, no pack/unpack) +- **Emplace/modify with same value size** -- no reallocation +- **Iterating forward** (`begin()` to `end()`, `++`) -- sequential access pattern, cache-friendly +- **SSO keys (<=24 bytes)** -- no heap allocation, inline in chainbase object +- **8-byte integer keys** -- single `bswap64` comparison instruction +- **Scope-based partitioning** -- each scope has its own key prefix, avoids scanning unrelated data + +### What's Slow + +- **Reverse iteration** (`rbegin()`/`rend()`) -- requires seeking to end first, then stepping backward. Use forward iteration where possible +- **Secondary index lookups** -- two-step: find in secondary index, then load primary row. ~2x the cost of a primary lookup +- **Large values (>1KB)** -- heap allocation + copy on every read/write. Keep row sizes small +- **Serialization overhead** (multi\_index only) -- pack/unpack on every read/write. Use `kv::table` with POD structs for hot paths +- **Creating many secondary indices** -- each index doubles the write cost (one KV write + one secondary index write per index per row). Only add indices you actually query +- **Table scans** -- iterating the entire table is O(n). Design your key structure to enable prefix-based range queries instead +- **Frequent erase + re-insert** -- generates chainbase undo history. Prefer `modify` over erase+emplace when updating + +### Design Tips + +- **Keep primary keys small** -- prefer `uint64_t` (8 bytes). The 24-byte key layout is `[table:8][scope:8][pk:8]` +- **Use scope for logical partitioning** -- e.g. sysio.token uses account name as scope for the `accounts` table, so balance lookups only scan one account's rows +- **Minimize secondary indices** -- each secondary index adds write amplification and storage. Only create indices you actively query +- **Use `kv::table` for simple CRUD** -- if you don't need secondary indices, `kv::table` avoids serialization overhead for POD types +- **Batch reads into single transaction** -- multiple `get()` calls in one action share the same chainbase session +- **Avoid storing large blobs** -- values are limited to 256 KiB; prefer multiple smaller rows over single large entries +- **Use `lower_bound` instead of scanning** -- for range queries, `lower_bound(start_key)` + iterate is much faster than scanning from `begin()` +- **secondary\_index\_view::find() with correct type** -- pass the exact secondary key type (e.g. `uint64_t(42)` not `42`) to avoid template-deduced type mismatch. The API handles coercion but explicit types are clearer + +--- + +## Migration Guide + +### From EOSIO multi\_index + +No code changes are required. Simply recompile your contract with the new Wire CDT: + +1. `#include ` now provides `sysio::kv_multi_index`, aliased as `sysio::multi_index` +2. `#include ` now provides `sysio::kv_singleton`, aliased as `sysio::singleton` +3. All existing API calls work unchanged: + - `emplace`, `modify`, `erase` + - `find`, `require_find`, `get` + - `lower_bound`, `upper_bound` + - `begin`/`end`, `rbegin`/`rend` + - `get_index<>()` with `find`, `lower_bound`, `modify`, `erase` + - `available_primary_key()` +4. The `payer` parameter is accepted but ignored -- RAM is charged to the contract account +5. `same_payer` is still defined and accepted + +### From kv::table to multi\_index + +If you started with `kv::table` and later need secondary indices: + +1. Change `#include ` to `#include ` +2. Add `SYSLIB_SERIALIZE` if not already present +3. Replace `kv::table` with `multi_index>` +4. Replace `set(pk, obj)` / `contains(pk)` with `emplace`/`modify`/`find` patterns +5. Note: `begin_all_scopes()` is only available on `kv::table` + +--- + +## Choosing Between APIs + +| Feature | multi\_index | kv::table | +|---------|-------------|-----------| +| Drop-in replacement for existing contracts | Yes | No | +| Secondary indices (via `kv_idx_*` intrinsics) | Yes (up to 16) | No | +| Reverse iteration | Yes (`rbegin`/`rend`) | Yes (bidirectional iterators) | +| Zero-copy for POD types | No (always serializes) | Yes | +| Object caching | Yes | No | +| Singleton support | Yes (`sysio::singleton`) | No (but can wrap manually) | +| Cross-scope iteration | No | Yes (`begin_all_scopes`) | +| `set(pk, obj)` convenience | No | Yes | +| `contains(pk)` | No (use `find`) | Yes (single intrinsic) | +| Best for | General purpose, complex queries | Hot path, simple CRUD | + +**Decision matrix:** + +- Need secondary indices or complex queries? Use **multi\_index**. +- Migrating an existing contract? Use **multi\_index** (zero code changes). +- Need maximum throughput on simple reads/writes with POD structs? Use **kv::table**. +- Need to iterate across all scopes? Use **kv::table** (`begin_all_scopes`). +- Need a simple ordered key-value store with custom keys? Use **kv::raw_table**. +- Not sure? Start with **multi\_index**. You can always switch later. + +--- + +## sysio::kv::raw_table (ordered key-value store with custom keys) + +### Include + +```cpp +#include +``` + +### Overview + +`sysio::kv::raw_table` provides an ordered key-value store similar to RocksDB or LevelDB. Define your key type, store key-value pairs, and iterate in key order. No scope or prefix overhead. + +Key properties: + +- **Custom key types**: keys can be any struct with `SYSLIB_SERIALIZE` +- **Big-endian key encoding**: keys are BE-encoded for correct `memcmp` ordering +- **String/binary key support**: strings and `vector` use NUL-escape encoding, preserving lexicographic order for arbitrary byte sequences (including embedded null bytes) +- **Ordered iteration**: `begin`/`end`, `lower_bound`/`upper_bound` +- **Standard ABI values**: values use LE serialization, decodable by SHiP clients via `abieos` +- Uses format=0 raw KV storage (`contract_row_kv` SHiP delta type) +- Keys <= 24 bytes benefit from the host's SSO fast-path (inline storage, integer comparison) + +> **Flat namespace warning:** All `raw_table` instances on the same contract share a single flat namespace. If your contract uses multiple `raw_table` instances, add a discriminator field to your key struct to prevent collisions. See the warning comment in `` for details and examples. + +### API reference + +| Method | Description | +|--------|-------------| +| `set(key, value)` | Store or update a key-value pair | +| `get(key)` | Returns `std::optional`, empty if not found | +| `contains(key)` | Returns `bool` | +| `erase(key)` | Delete a key-value pair, returns RAM delta | +| `begin()` / `end()` | Forward iteration over all entries | +| `lower_bound(key)` | First entry with key >= given key | +| `upper_bound(key)` | First entry with key > given key | + +### ABI key metadata + +To declare the key type in the ABI (so SHiP clients can decode raw keys), use `[[sysio::kv_key("key_struct")]]` on the value struct: + +```cpp +struct my_key { + std::string region; + uint64_t id; + SYSLIB_SERIALIZE(my_key, (region)(id)) +}; + +struct [[sysio::table("geodata"), sysio::kv_key("my_key")]] my_value { + std::string payload; + uint64_t amount; + SYSLIB_SERIALIZE(my_value, (payload)(amount)) +}; + +kv::raw_table geodata; // reads own contract data +kv::raw_table other("othercon"_n); // reads another contract's data (read-only) +``` + +This generates an ABI table entry with key field metadata: + +```json +{ + "name": "geodata", + "type": "my_value", + "key_names": ["region", "id"], + "key_types": ["string", "uint64"] +} +``` + +The `my_key` struct is also added to the ABI's `structs` section. + +### Key encoding details + +Keys are encoded in big-endian byte order so that `memcmp`-based comparison in the underlying store produces correct lexicographic ordering: + +| Type | Encoding | +|------|----------| +| `uint8` | 1 byte | +| `uint16` | 2 bytes BE | +| `uint32` | 4 bytes BE | +| `uint64` / `name` | 8 bytes BE | +| `uint128` | 16 bytes BE | +| `double` | 8 bytes (IEEE 754 with sign-flip for sort order) | +| `string` | Raw bytes + `0x00` terminator (no embedded nulls) | +| `bool` | 1 byte (0 or 1) | + +Composite key structs are encoded by concatenating each field in declaration order using the above rules. The `SYSLIB_SERIALIZE` macro's field order determines the encoding order. + +### Example + +```cpp +#include +#include + +using namespace sysio; + +class [[sysio::contract]] inventory : public contract { +public: + using contract::contract; + + struct my_key { + std::string region; + uint64_t id; + SYSLIB_SERIALIZE(my_key, (region)(id)) + }; + + struct [[sysio::table("items"), sysio::kv_key("my_key")]] item { + std::string name; + uint64_t quantity; + SYSLIB_SERIALIZE(item, (name)(quantity)) + }; + + kv::raw_table items; + + [[sysio::action]] + void additem(std::string region, uint64_t id, std::string name, uint64_t qty) { + items.set({region, id}, {name, qty}); + } + + [[sysio::action]] + void getitem(std::string region, uint64_t id) { + auto val = items.get({region, id}); + check(val.has_value(), "item not found"); + } +}; +``` diff --git a/libraries/native/intrinsics.cpp b/libraries/native/intrinsics.cpp index ffe597648..bfcb2e54a 100644 --- a/libraries/native/intrinsics.cpp +++ b/libraries/native/intrinsics.cpp @@ -52,186 +52,6 @@ extern "C" { uint32_t get_active_producers( capi_name* producers, uint32_t datalen ) { return intrinsics::get().call(producers, datalen); } - int32_t db_idx64_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const uint64_t* secondary) { - return intrinsics::get().call(scope, table, payer, id, secondary); - } - void db_idx64_remove(int32_t iterator) { - return intrinsics::get().call(iterator); - } - void db_idx64_update(int32_t iterator, capi_name payer, const uint64_t* secondary) { - return intrinsics::get().call(iterator, payer, secondary); - } - int32_t db_idx64_find_primary(capi_name code, uint64_t scope, capi_name table, uint64_t* secondary, uint64_t primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx64_find_secondary(capi_name code, uint64_t scope, capi_name table, const uint64_t* secondary, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx64_lowerbound(capi_name code, uint64_t scope, capi_name table, uint64_t* secondary, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx64_upperbound(capi_name code, uint64_t scope, capi_name table, uint64_t* secondary, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx64_end(capi_name code, uint64_t scope, capi_name table) { - return intrinsics::get().call(code, scope, table); - } - int32_t db_idx64_next(int32_t iterator, uint64_t* primary) { - return intrinsics::get().call(iterator, primary); - } - int32_t db_idx64_previous(int32_t iterator, uint64_t* primary) { - return intrinsics::get().call(iterator, primary); - } - int32_t db_idx128_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const uint128_t* secondary) { - return intrinsics::get().call(scope, table, payer, id, secondary); - } - void db_idx128_remove(int32_t iterator) { - return intrinsics::get().call(iterator); - } - void db_idx128_update(int32_t iterator, capi_name payer, const uint128_t* secondary) { - return intrinsics::get().call(iterator, payer, secondary); - } - int32_t db_idx128_find_primary(capi_name code, uint64_t scope, capi_name table, uint128_t* secondary, uint64_t primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx128_find_secondary(capi_name code, uint64_t scope, capi_name table, const uint128_t* secondary, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx128_lowerbound(capi_name code, uint64_t scope, capi_name table, uint128_t* secondary, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx128_upperbound(capi_name code, uint64_t scope, capi_name table, uint128_t* secondary, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx128_end(capi_name code, uint64_t scope, capi_name table) { - return intrinsics::get().call(code, scope, table); - } - int32_t db_idx128_next(int32_t iterator, uint64_t* primary) { - return intrinsics::get().call(iterator, primary); - } - int32_t db_idx128_previous(int32_t iterator, uint64_t* primary) { - return intrinsics::get().call(iterator, primary); - } - int32_t db_idx256_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const uint128_t* data, uint32_t datalen) { - return intrinsics::get().call(scope, table, payer, id, data, datalen); - } - void db_idx256_remove(int32_t iterator) { - return intrinsics::get().call(iterator); - } - void db_idx256_update(int32_t iterator, capi_name payer, const uint128_t* data, uint32_t datalen) { - return intrinsics::get().call(iterator, payer, data, datalen); - } - int32_t db_idx256_find_primary(capi_name code, uint64_t scope, capi_name table, uint128_t* data, uint32_t datalen, uint64_t primary) { - return intrinsics::get().call(code, scope, table, data, datalen, primary); - } - int32_t db_idx256_find_secondary(capi_name code, uint64_t scope, capi_name table, const uint128_t* data, uint32_t datalen, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, data, datalen, primary); - } - int32_t db_idx256_lowerbound(capi_name code, uint64_t scope, capi_name table, uint128_t* data, uint32_t datalen, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, data, datalen, primary); - } - int32_t db_idx256_upperbound(capi_name code, uint64_t scope, capi_name table, uint128_t* data, uint32_t datalen, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, data, datalen, primary); - } - int32_t db_idx256_end(capi_name code, uint64_t scope, capi_name table) { - return intrinsics::get().call(code, scope, table); - } - int32_t db_idx256_next(int32_t iterator, uint64_t* primary) { - return intrinsics::get().call(iterator, primary); - } - int32_t db_idx256_previous(int32_t iterator, uint64_t* primary) { - return intrinsics::get().call(iterator, primary); - } - int32_t db_idx_double_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const double* secondary) { - return intrinsics::get().call(scope, table, payer, id, secondary); - } - void db_idx_double_remove(int32_t iterator) { - return intrinsics::get().call(iterator); - } - void db_idx_double_update(int32_t iterator, capi_name payer, const double* secondary) { - return intrinsics::get().call(iterator, payer, secondary); - } - int32_t db_idx_double_find_primary(capi_name code, uint64_t scope, capi_name table, double* secondary, uint64_t primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx_double_find_secondary(capi_name code, uint64_t scope, capi_name table, const double* secondary, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx_double_lowerbound(capi_name code, uint64_t scope, capi_name table, double* secondary, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx_double_upperbound(capi_name code, uint64_t scope, capi_name table, double* secondary, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx_double_end(capi_name code, uint64_t scope, capi_name table) { - return intrinsics::get().call(code, scope, table); - } - int32_t db_idx_double_next(int32_t iterator, uint64_t* primary) { - return intrinsics::get().call(iterator, primary); - } - int32_t db_idx_double_previous(int32_t iterator, uint64_t* primary) { - return intrinsics::get().call(iterator, primary); - } - int32_t db_idx_long_double_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const long double* secondary) { - return intrinsics::get().call(scope, table, payer, id, secondary); - } - void db_idx_long_double_remove(int32_t iterator) { - return intrinsics::get().call(iterator); - } - void db_idx_long_double_update(int32_t iterator, capi_name payer, const long double* secondary) { - return intrinsics::get().call(iterator, payer, secondary); - } - int32_t db_idx_long_double_find_primary(capi_name code, uint64_t scope, capi_name table, long double* secondary, uint64_t primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx_long_double_find_secondary(capi_name code, uint64_t scope, capi_name table, const long double* secondary, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx_long_double_lowerbound(capi_name code, uint64_t scope, capi_name table, long double* secondary, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx_long_double_upperbound(capi_name code, uint64_t scope, capi_name table, long double* secondary, uint64_t* primary) { - return intrinsics::get().call(code, scope, table, secondary, primary); - } - int32_t db_idx_long_double_end(capi_name code, uint64_t scope, capi_name table) { - return intrinsics::get().call(code, scope, table); - } - int32_t db_idx_long_double_next(int32_t iterator, uint64_t* primary) { - return intrinsics::get().call(iterator, primary); - } - int32_t db_idx_long_double_previous(int32_t iterator, uint64_t* primary) { - return intrinsics::get().call(iterator, primary); - } - int32_t db_store_i64(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const void* data, uint32_t len) { - return intrinsics::get().call(scope, table, payer, id, data, len); - } - void db_update_i64(int32_t iterator, capi_name payer, const void* data, uint32_t len) { - return intrinsics::get().call(iterator, payer, data, len); - } - void db_remove_i64(int32_t iterator) { - return intrinsics::get().call(iterator); - } - int32_t db_get_i64(int32_t iterator, const void* data, uint32_t len) { - return intrinsics::get().call(iterator, data, len); - } - int32_t db_next_i64(int32_t iterator, uint64_t* primary) { - return intrinsics::get().call(iterator, primary); - } - int32_t db_previous_i64(int32_t iterator, uint64_t* primary) { - return intrinsics::get().call(iterator, primary); - } - int32_t db_find_i64(capi_name code, uint64_t scope, capi_name table, uint64_t id) { - return intrinsics::get().call(code, scope, table, id); - } - int32_t db_lowerbound_i64(capi_name code, uint64_t scope, capi_name table, uint64_t id) { - return intrinsics::get().call(code, scope, table, id); - } - int32_t db_upperbound_i64(capi_name code, uint64_t scope, capi_name table, uint64_t id) { - return intrinsics::get().call(code, scope, table, id); - } - int32_t db_end_i64(capi_name code, uint64_t scope, capi_name table) { - return intrinsics::get().call(code, scope, table); - } void assert_recover_key( const capi_checksum256* digest, const char* sig, size_t siglen, const char* pub, size_t publen ) { return intrinsics::get().call(digest, sig, siglen, pub, publen); } @@ -984,3 +804,102 @@ int32_t bls_fp_exp(const char* base, uint32_t base_len, const char* exp, uint32_ { return intrinsics::get().call(base, base_len, exp, exp_len, res, res_len); } + +// --- KV Database intrinsics --- + +int64_t kv_set(uint32_t key_format, uint64_t payer, const void* key, uint32_t key_size, const void* value, uint32_t value_size) { + return intrinsics::get().call(key_format, payer, key, key_size, value, value_size); +} + +int32_t kv_get(uint32_t key_format, capi_name code, const void* key, uint32_t key_size, void* value, uint32_t value_size) { + return intrinsics::get().call(key_format, code, key, key_size, value, value_size); +} + +int64_t kv_erase(uint32_t key_format, const void* key, uint32_t key_size) { + return intrinsics::get().call(key_format, key, key_size); +} + +int32_t kv_contains(uint32_t key_format, capi_name code, const void* key, uint32_t key_size) { + return intrinsics::get().call(key_format, code, key, key_size); +} + +uint32_t kv_it_create(uint32_t key_format, capi_name code, const void* prefix, uint32_t prefix_size) { + return intrinsics::get().call(key_format, code, prefix, prefix_size); +} + +void kv_it_destroy(uint32_t handle) { + intrinsics::get().call(handle); +} + +int32_t kv_it_status(uint32_t handle) { + return intrinsics::get().call(handle); +} + +int32_t kv_it_next(uint32_t handle) { + return intrinsics::get().call(handle); +} + +int32_t kv_it_prev(uint32_t handle) { + return intrinsics::get().call(handle); +} + +int32_t kv_it_lower_bound(uint32_t handle, const void* key, uint32_t key_size) { + return intrinsics::get().call(handle, key, key_size); +} + +int32_t kv_it_key(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size) { + return intrinsics::get().call(handle, offset, dest, dest_size, actual_size); +} + +int32_t kv_it_value(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size) { + return intrinsics::get().call(handle, offset, dest, dest_size, actual_size); +} + +void kv_idx_store(uint64_t payer, capi_name table, uint32_t index_id, + const void* pri_key, uint32_t pri_key_size, + const void* sec_key, uint32_t sec_key_size) { + intrinsics::get().call(payer, table, index_id, pri_key, pri_key_size, sec_key, sec_key_size); +} + +void kv_idx_remove(capi_name table, uint32_t index_id, + const void* pri_key, uint32_t pri_key_size, + const void* sec_key, uint32_t sec_key_size) { + intrinsics::get().call(table, index_id, pri_key, pri_key_size, sec_key, sec_key_size); +} + +void kv_idx_update(uint64_t payer, capi_name table, uint32_t index_id, + const void* pri_key, uint32_t pri_key_size, + const void* old_sec_key, uint32_t old_sec_key_size, + const void* new_sec_key, uint32_t new_sec_key_size) { + intrinsics::get().call(payer, table, index_id, pri_key, pri_key_size, old_sec_key, old_sec_key_size, new_sec_key, new_sec_key_size); +} + +int32_t kv_idx_find_secondary(capi_name code, capi_name table, uint32_t index_id, + const void* sec_key, uint32_t sec_key_size) { + return intrinsics::get().call(code, table, index_id, sec_key, sec_key_size); +} + +int32_t kv_idx_lower_bound(capi_name code, capi_name table, uint32_t index_id, + const void* sec_key, uint32_t sec_key_size) { + return intrinsics::get().call(code, table, index_id, sec_key, sec_key_size); +} + +int32_t kv_idx_next(uint32_t handle) { + return intrinsics::get().call(handle); +} + +int32_t kv_idx_prev(uint32_t handle) { + return intrinsics::get().call(handle); +} + +int32_t kv_idx_key(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size) { + return intrinsics::get().call(handle, offset, dest, dest_size, actual_size); +} + +int32_t kv_idx_primary_key(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size) { + return intrinsics::get().call(handle, offset, dest, dest_size, actual_size); +} + +void kv_idx_destroy(uint32_t handle) { + intrinsics::get().call(handle); +} diff --git a/libraries/native/native/sysio/intrinsics_def.hpp b/libraries/native/native/sysio/intrinsics_def.hpp index 9ed2ac3f8..560d2ed70 100644 --- a/libraries/native/native/sysio/intrinsics_def.hpp +++ b/libraries/native/native/sysio/intrinsics_def.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -56,66 +57,6 @@ intrinsic_macro(set_privileged) \ intrinsic_macro(is_feature_activated) \ intrinsic_macro(preactivate_feature) \ intrinsic_macro(get_active_producers) \ -intrinsic_macro(db_idx64_store) \ -intrinsic_macro(db_idx64_remove) \ -intrinsic_macro(db_idx64_update) \ -intrinsic_macro(db_idx64_find_primary) \ -intrinsic_macro(db_idx64_find_secondary) \ -intrinsic_macro(db_idx64_lowerbound) \ -intrinsic_macro(db_idx64_upperbound) \ -intrinsic_macro(db_idx64_end) \ -intrinsic_macro(db_idx64_next) \ -intrinsic_macro(db_idx64_previous) \ -intrinsic_macro(db_idx128_store) \ -intrinsic_macro(db_idx128_remove) \ -intrinsic_macro(db_idx128_update) \ -intrinsic_macro(db_idx128_find_primary) \ -intrinsic_macro(db_idx128_find_secondary) \ -intrinsic_macro(db_idx128_lowerbound) \ -intrinsic_macro(db_idx128_upperbound) \ -intrinsic_macro(db_idx128_end) \ -intrinsic_macro(db_idx128_next) \ -intrinsic_macro(db_idx128_previous) \ -intrinsic_macro(db_idx256_store) \ -intrinsic_macro(db_idx256_remove) \ -intrinsic_macro(db_idx256_update) \ -intrinsic_macro(db_idx256_find_primary) \ -intrinsic_macro(db_idx256_find_secondary) \ -intrinsic_macro(db_idx256_lowerbound) \ -intrinsic_macro(db_idx256_upperbound) \ -intrinsic_macro(db_idx256_end) \ -intrinsic_macro(db_idx256_next) \ -intrinsic_macro(db_idx256_previous) \ -intrinsic_macro(db_idx_double_store) \ -intrinsic_macro(db_idx_double_remove) \ -intrinsic_macro(db_idx_double_update) \ -intrinsic_macro(db_idx_double_find_primary) \ -intrinsic_macro(db_idx_double_find_secondary) \ -intrinsic_macro(db_idx_double_lowerbound) \ -intrinsic_macro(db_idx_double_upperbound) \ -intrinsic_macro(db_idx_double_end) \ -intrinsic_macro(db_idx_double_next) \ -intrinsic_macro(db_idx_double_previous) \ -intrinsic_macro(db_idx_long_double_store) \ -intrinsic_macro(db_idx_long_double_remove) \ -intrinsic_macro(db_idx_long_double_update) \ -intrinsic_macro(db_idx_long_double_find_primary) \ -intrinsic_macro(db_idx_long_double_find_secondary) \ -intrinsic_macro(db_idx_long_double_lowerbound) \ -intrinsic_macro(db_idx_long_double_upperbound) \ -intrinsic_macro(db_idx_long_double_end) \ -intrinsic_macro(db_idx_long_double_next) \ -intrinsic_macro(db_idx_long_double_previous) \ -intrinsic_macro(db_store_i64) \ -intrinsic_macro(db_update_i64) \ -intrinsic_macro(db_remove_i64) \ -intrinsic_macro(db_get_i64) \ -intrinsic_macro(db_next_i64) \ -intrinsic_macro(db_previous_i64) \ -intrinsic_macro(db_find_i64) \ -intrinsic_macro(db_lowerbound_i64) \ -intrinsic_macro(db_upperbound_i64) \ -intrinsic_macro(db_end_i64) \ intrinsic_macro(assert_recover_key) \ intrinsic_macro(recover_key) \ intrinsic_macro(assert_sha256) \ @@ -184,7 +125,29 @@ intrinsic_macro(bls_fp_mod) \ intrinsic_macro(bls_fp_mul) \ intrinsic_macro(bls_fp_exp) \ intrinsic_macro(set_finalizers) \ -intrinsic_macro(get_ram_usage) +intrinsic_macro(get_ram_usage) \ +intrinsic_macro(kv_set) \ +intrinsic_macro(kv_get) \ +intrinsic_macro(kv_erase) \ +intrinsic_macro(kv_contains) \ +intrinsic_macro(kv_it_create) \ +intrinsic_macro(kv_it_destroy) \ +intrinsic_macro(kv_it_status) \ +intrinsic_macro(kv_it_next) \ +intrinsic_macro(kv_it_prev) \ +intrinsic_macro(kv_it_lower_bound) \ +intrinsic_macro(kv_it_key) \ +intrinsic_macro(kv_it_value) \ +intrinsic_macro(kv_idx_store) \ +intrinsic_macro(kv_idx_remove) \ +intrinsic_macro(kv_idx_update) \ +intrinsic_macro(kv_idx_find_secondary) \ +intrinsic_macro(kv_idx_lower_bound) \ +intrinsic_macro(kv_idx_next) \ +intrinsic_macro(kv_idx_prev) \ +intrinsic_macro(kv_idx_key) \ +intrinsic_macro(kv_idx_primary_key) \ +intrinsic_macro(kv_idx_destroy) #define CREATE_ENUM(name) \ name, diff --git a/libraries/sysiolib/capi/sysio/db.h b/libraries/sysiolib/capi/sysio/db.h index 3a41a27ed..3cd3e4871 100644 --- a/libraries/sysiolib/capi/sysio/db.h +++ b/libraries/sysiolib/capi/sysio/db.h @@ -1,812 +1,12 @@ /** * @file db.h * @copyright defined in eos/LICENSE - * @brief Defines C API for interfacing with blockchain database - */ -#pragma once - -#include "types.h" -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @addtogroup database_c_api Database C API - * @ingroup c_api - * @brief Defines %C APIs for interfacing with the database. - * @details Database C API provides low level interface to SYSIO database. + * @brief Legacy database C API — replaced by KV intrinsics * - * @section tabletypes Supported Table Types - * Following are the table types supported by the C API: - * 1. Primary Table - * - 64-bit integer key - * 2. Secondary Index Table - * - 64-bit integer key - * - 128-bit integer key - * - 256-bit integer key - * - double key - * - long double key - * @{ + * The legacy db_*_i64 and db_idx* functions have been removed. + * All contract storage now uses the KV database API declared in . + * The C++ wrapper is available via (drop-in replacement). */ +#pragma once -/** - * @brief Store a record in a primary 64-bit integer index table - * @param scope - The scope where the table resides (implied to be within the code of the current receiver) - * @param table - The table name - * @param payer - The account that pays for the storage costs - * @param id - ID of the entry - * @param data - Record to store - * @param len - Size of data - * @pre `data` is a valid pointer to a range of memory at least `len` bytes long - * @pre `*((uint64_t*)data)` stores the primary key - * @return iterator to the newly created table row - * @post a new entry is created in the table - */ -__attribute__((sysio_wasm_import)) -int32_t db_store_i64(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const void* data, uint32_t len); - -/** - * @brief Update a record in a primary 64-bit integer index table - * @param iterator - Iterator to the table row containing the record to update - * @param payer - The account that pays for the storage costs (use 0 to continue using current payer) - * @param data - New updated record - * @param len - Size of data - * @pre `data` is a valid pointer to a range of memory at least `len` bytes long - * @pre `*((uint64_t*)data)` stores the primary key - * @pre `iterator` points to an existing table row in the table - * @post the record contained in the table row pointed to by `iterator` is replaced with the new updated record - * @remark This function does not allow changing the primary key of a - * table row. The serialized data that is stored in the table row of a - * primary table may include a primary key and that primary key value - * could be changed by the contract calling the db_update_i64 intrinsic; - * but that does not change the actual primary key of the table row. - */ -__attribute__((sysio_wasm_import)) -void db_update_i64(int32_t iterator, capi_name payer, const void* data, uint32_t len); - -/** - * @brief Remove a record from a primary 64-bit integer index table - * @param iterator - Iterator to the table row to remove - * @pre `iterator` points to an existing table row in the table - * @post the table row pointed to by `iterator` is removed and the associated storage costs are refunded to the payer - * - * Example: - * - * @code - * int32_t itr = db_find_i64(receiver, receiver, table1, "alice"_n); - * sysio_assert(itr >= 0, "Alice cannot be removed since she was already not found in the table"); - * db_remove_i64(itr); - * @endcode - */ -__attribute__((sysio_wasm_import)) -void db_remove_i64(int32_t iterator); - -/** - * @brief Get a record in a primary 64-bit integer index table - * @param iterator - The iterator to the table row containing the record to retrieve - * @param data - Pointer to the buffer which will be filled with the retrieved record - * @param len - Size of the buffer - * @return size of the data copied into the buffer if `len > 0`, or size of the retrieved record if `len == 0`. - * @pre `iterator` points to an existing table row in the table - * @pre `data` is a valid pointer to a range of memory at least `len` bytes long - * @post `data` will be filled with the retrieved record (truncated to the first `len` bytes if necessary) - * - * Example: - * - * @code - * char value[50]; - * auto len = db_get_i64(itr, value, 0); - * sysio_assert(len <= 50, "buffer to small to store retrieved record"); - * db_get_i64(itr, value, len); - * @endcode - */ -__attribute__((sysio_wasm_import)) -int32_t db_get_i64(int32_t iterator, const void* data, uint32_t len); - -/** - * @brief Find the table row following the referenced table row in a primary 64-bit integer index table - * @param iterator - The iterator to the referenced table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the next table row - * @return iterator to the table row following the referenced table row (or the end iterator of the table if the referenced table row is the last one in the table) - * @pre `iterator` points to an existing table row in the table - * @post `*primary` will be replaced with the primary key of the table row following the referenced table row if it exists, otherwise `*primary` will be left untouched - * - * Example: - * - * @code - * int32_t charlie_itr = db_find_i64(receiver, receiver, table1, "charlie"_n); - * // expect nothing after charlie - * uint64_t prim = 0 - * int32_t end_itr = db_next_i64(charlie_itr, &prim); - * sysio_assert(end_itr < -1, "Charlie was not the last entry in the table"); - * @endcode - */ -__attribute__((sysio_wasm_import)) -int32_t db_next_i64(int32_t iterator, uint64_t* primary); - -/** - * @brief Find the table row preceding the referenced table row in a primary 64-bit integer index table - * @param iterator - The iterator to the referenced table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the previous table row - * @return iterator to the table row preceding the referenced table row assuming one exists (it will return -1 if the referenced table row is the first one in the table) - * @pre `iterator` points to an existing table row in the table or it is the end iterator of the table - * @post `*primary` will be replaced with the primary key of the table row preceding the referenced table row if it exists, otherwise `*primary` will be left untouched - * - * Example: - * - * @code - * uint64_t prim = 0; - * int32_t itr_prev = db_previous_i64(itr, &prim); - * @endcode - */ -__attribute__((sysio_wasm_import)) -int32_t db_previous_i64(int32_t iterator, uint64_t* primary); - -/** - * @brief Find a table row in a primary 64-bit integer index table by primary key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param id - The primary key of the table row to look up - * @return iterator to the table row with a primary key equal to `id` or the end iterator of the table if the table row could not be found - * - * Example: - * - * @code - * int itr = db_find_i64(receiver, receiver, table1, "charlie"_n); - * @endcode - */ -__attribute__((sysio_wasm_import)) -int32_t db_find_i64(capi_name code, uint64_t scope, capi_name table, uint64_t id); - -/** - * @brief Find the table row in a primary 64-bit integer index table that matches the lowerbound condition for a given primary key - * @details The table row that matches the lowerbound condition is the first table row in the table with the lowest primary key that is >= the given key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param id - The primary key used to determine the lowerbound - * @return iterator to the found table row or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_lowerbound_i64(capi_name code, uint64_t scope, capi_name table, uint64_t id); - -/** - * @brief Find the table row in a primary 64-bit integer index table that matches the upperbound condition for a given primary key - * @details The table row that matches the upperbound condition is the first table row in the table with the lowest primary key that is > the given key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param id - The primary key used to determine the upperbound - * @return iterator to the found table row or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_upperbound_i64(capi_name code, uint64_t scope, capi_name table, uint64_t id); - -/** - * @brief Get an iterator representing just-past-the-end of the last table row of a primary 64-bit integer index table - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @return end iterator of the table - */ -__attribute__((sysio_wasm_import)) -int32_t db_end_i64(capi_name code, uint64_t scope, capi_name table); - -/** - * @brief Store an association of a 64-bit integer secondary key to a primary key in a secondary 64-bit integer index table - * @param scope - The scope where the table resides (implied to be within the code of the current receiver) - * @param table - The table name - * @param payer - The account that pays for the storage costs - * @param id - The primary key to which to associate the secondary key - * @param secondary - Pointer to the secondary key - * @return iterator to the newly created table row - * @post new secondary key association between primary key `id` and secondary key `*secondary` is created in the secondary 64-bit integer index table - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx64_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const uint64_t* secondary); - -/** - * @brief Update an association for a 64-bit integer secondary key to a primary key in a secondary 64-bit integer index table - * @param iterator - The iterator to the table row containing the secondary key association to update - * @param payer - The account that pays for the storage costs (use 0 to continue using current payer) - * @param secondary - Pointer to the **new** secondary key that will replace the existing one of the association - * @pre `iterator` points to an existing table row in the table - * @post the secondary key of the table row pointed to by `iterator` is replaced by `*secondary` - */ -__attribute__((sysio_wasm_import)) -void db_idx64_update(int32_t iterator, capi_name payer, const uint64_t* secondary); - -/** - * @brief Remove a table row from a secondary 64-bit integer index table - * @param iterator - Iterator to the table row to remove - * @pre `iterator` points to an existing table row in the table - * @post the table row pointed to by `iterator` is removed and the associated storage costs are refunded to the payer - */ -__attribute__((sysio_wasm_import)) -void db_idx64_remove(int32_t iterator); - -/** - * @brief Find the table row following the referenced table row in a secondary 64-bit integer index table - * @param iterator - The iterator to the referenced table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the next table row - * @return iterator to the table row following the referenced table row (or the end iterator of the table if the referenced table row is the last one in the table) - * @pre `iterator` points to an existing table row in the table - * @post `*primary` will be replaced with the primary key of the table row following the referenced table row if it exists, otherwise `*primary` will be left untouched - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx64_next(int32_t iterator, uint64_t* primary); - -/** - * @brief Find the table row preceding the referenced table row in a secondary 64-bit integer index table - * @param iterator - The iterator to the referenced table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the previous table row - * @return iterator to the table row preceding the referenced table row assuming one exists (it will return -1 if the referenced table row is the first one in the table) - * @pre `iterator` points to an existing table row in the table or it is the end iterator of the table - * @post `*primary` will be replaced with the primary key of the table row preceding the referenced table row if it exists, otherwise `*primary` will be left untouched - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx64_previous(int32_t iterator, uint64_t* primary); - -/** - * @brief Find a table row in a secondary 64-bit integer index table by primary key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to a `uint64_t` variable which will have its value set to the secondary key of the found table row - * @param primary - The primary key of the table row to look up - * @post If and only if the table row is found, `*secondary` will be replaced with the secondary key of the found table row - * @return iterator to the table row with a primary key equal to `id` or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx64_find_primary(capi_name code, uint64_t scope, capi_name table, uint64_t* secondary, uint64_t primary); - -/** - * @brief Find a table row in a secondary 64-bit integer index table by secondary key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to secondary key used to lookup the table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the first table row with a secondary key equal to `*secondary` or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx64_find_secondary(capi_name code, uint64_t scope, capi_name table, const uint64_t* secondary, uint64_t* primary); - -/** - * @brief Find the table row in a secondary 64-bit integer index table that matches the lowerbound condition for a given secondary key - * @details The table row that matches the lowerbound condition is the first table row in the table with the lowest secondary key that is >= the given key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to secondary key first used to determine the lowerbound and which is then replaced with the secondary key of the found table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*secondary` will be replaced with the secondary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the found table row or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx64_lowerbound(capi_name code, uint64_t scope, capi_name table, uint64_t* secondary, uint64_t* primary); - -/** - * @brief Find the table row in a secondary 64-bit integer index table that matches the upperbound condition for a given secondary key - * @details The table row that matches the upperbound condition is the first table row in the table with the lowest secondary key that is > the given key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to secondary key first used to determine the upperbound and which is then replaced with the secondary key of the found table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*secondary` will be replaced with the secondary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the found table row or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx64_upperbound(capi_name code, uint64_t scope, capi_name table, uint64_t* secondary, uint64_t* primary); - -/** - * @brief Get an end iterator representing just-past-the-end of the last table row of a secondary 64-bit integer index table - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @return end iterator of the table - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx64_end(capi_name code, uint64_t scope, capi_name table); - - - -/** - * @brief Store an association of a 128-bit integer secondary key to a primary key in a secondary 128-bit integer index table - * @param scope - The scope where the table resides (implied to be within the code of the current receiver) - * @param table - The table name - * @param payer - The account that pays for the storage costs - * @param id - The primary key to which to associate the secondary key - * @param secondary - Pointer to the secondary key - * @return iterator to the newly created table row - * @post new secondary key association between primary key `id` and secondary key `*secondary` is created in the secondary 128-bit integer index table - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx128_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const uint128_t* secondary); - -/** - * @brief Update an association for a 128-bit integer secondary key to a primary key in a secondary 128-bit integer index table - * @param iterator - The iterator to the table row containing the secondary key association to update - * @param payer - The account that pays for the storage costs (use 0 to continue using current payer) - * @param secondary - Pointer to the **new** secondary key that will replace the existing one of the association - * @pre `iterator` points to an existing table row in the table - * @post the secondary key of the table row pointed to by `iterator` is replaced by `*secondary` - */ -__attribute__((sysio_wasm_import)) -void db_idx128_update(int32_t iterator, capi_name payer, const uint128_t* secondary); - -/** - * @brief Remove a table row from a secondary 128-bit integer index table - * @param iterator - Iterator to the table row to remove - * @pre `iterator` points to an existing table row in the table - * @post the table row pointed to by `iterator` is removed and the associated storage costs are refunded to the payer - */ -__attribute__((sysio_wasm_import)) -void db_idx128_remove(int32_t iterator); - -/** - * @brief Find the table row following the referenced table row in a secondary 128-bit integer index table - * @param iterator - The iterator to the referenced table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the next table row - * @return iterator to the table row following the referenced table row (or the end iterator of the table if the referenced table row is the last one in the table) - * @pre `iterator` points to an existing table row in the table - * @post `*primary` will be replaced with the primary key of the table row following the referenced table row if it exists, otherwise `*primary` will be left untouched - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx128_next(int32_t iterator, uint64_t* primary); - -/** - * @brief Find the table row preceding the referenced table row in a secondary 128-bit integer index table - * @param iterator - The iterator to the referenced table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the previous table row - * @return iterator to the table row preceding the referenced table row assuming one exists (it will return -1 if the referenced table row is the first one in the table) - * @pre `iterator` points to an existing table row in the table or it is the end iterator of the table - * @post `*primary` will be replaced with the primary key of the table row preceding the referenced table row if it exists, otherwise `*primary` will be left untouched - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx128_previous(int32_t iterator, uint64_t* primary); - -/** - * @brief Find a table row in a secondary 128-bit integer index table by primary key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to a `uint128_t` variable which will have its value set to the secondary key of the found table row - * @param primary - The primary key of the table row to look up - * @post If and only if the table row is found, `*secondary` will be replaced with the secondary key of the found table row - * @return iterator to the table row with a primary key equal to `id` or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx128_find_primary(capi_name code, uint64_t scope, capi_name table, uint128_t* secondary, uint64_t primary); - -/** - * @brief Find a table row in a secondary 128-bit integer index table by secondary key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to secondary key used to lookup the table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the first table row with a secondary key equal to `*secondary` or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx128_find_secondary(capi_name code, uint64_t scope, capi_name table, const uint128_t* secondary, uint64_t* primary); - -/** - * @brief Find the table row in a secondary 128-bit integer index table that matches the lowerbound condition for a given secondary key - * @details The table row that matches the lowerbound condition is the first table row in the table with the lowest secondary key that is >= the given key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to secondary key first used to determine the lowerbound and which is then replaced with the secondary key of the found table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*secondary` will be replaced with the secondary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the found table row or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx128_lowerbound(capi_name code, uint64_t scope, capi_name table, uint128_t* secondary, uint64_t* primary); - -/** - * @brief Find the table row in a secondary 128-bit integer index table that matches the upperbound condition for a given secondary key - * @details The table row that matches the upperbound condition is the first table row in the table with the lowest secondary key that is > the given key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to secondary key first used to determine the upperbound and which is then replaced with the secondary key of the found table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*secondary` will be replaced with the secondary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the found table row or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx128_upperbound(capi_name code, uint64_t scope, capi_name table, uint128_t* secondary, uint64_t* primary); - -/** - * @brief Get an end iterator representing just-past-the-end of the last table row of a secondary 128-bit integer index table - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @return end iterator of the table - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx128_end(capi_name code, uint64_t scope, capi_name table); - -/** - * @brief Store an association of a 256-bit secondary key to a primary key in a secondary 256-bit index table - * @param scope - The scope where the table resides (implied to be within the code of the current receiver) - * @param table - The table name - * @param payer - The account that pays for the storage costs - * @param id - The primary key to which to associate the secondary key - * @param data - Pointer to the secondary key data stored as an array of 2 `uint128_t` integers - * @param data_len - Must be set to 2 - * @return iterator to the newly created table row - * @post new secondary key association between primary key `id` and the specified secondary key is created in the secondary 256-bit index table - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx256_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const uint128_t* data, uint32_t data_len ); - -/** - * @brief Update an association for a 256-bit secondary key to a primary key in a secondary 256-bit index table - * @param iterator - The iterator to the table row containing the secondary key association to update - * @param payer - The account that pays for the storage costs (use 0 to continue using current payer) - * @param data - Pointer to the **new** secondary key data (which is stored as an array of 2 `uint128_t` integers) that will replace the existing one of the association - * @param data_len - Must be set to 2 - * @pre `iterator` points to an existing table row in the table - * @post the secondary key of the table row pointed to by `iterator` is replaced by the specified secondary key - */ -__attribute__((sysio_wasm_import)) -void db_idx256_update(int32_t iterator, capi_name payer, const uint128_t* data, uint32_t data_len); - -/** - * @brief Remove a table row from a secondary 256-bit index table - * @param iterator - Iterator to the table row to remove - * @pre `iterator` points to an existing table row in the table - * @post the table row pointed to by `iterator` is removed and the associated storage costs are refunded to the payer - */ -__attribute__((sysio_wasm_import)) -void db_idx256_remove(int32_t iterator); - -/** - * @brief Find the table row following the referenced table row in a secondary 256-bit index table - * @param iterator - The iterator to the referenced table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the next table row - * @return iterator to the table row following the referenced table row (or the end iterator of the table if the referenced table row is the last one in the table) - * @pre `iterator` points to an existing table row in the table - * @post `*primary` will be replaced with the primary key of the table row following the referenced table row if it exists, otherwise `*primary` will be left untouched - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx256_next(int32_t iterator, uint64_t* primary); - -/** - * @brief Find the table row preceding the referenced table row in a secondary 256-bit index table - * @param iterator - The iterator to the referenced table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the previous table row - * @return iterator to the table row preceding the referenced table row assuming one exists (it will return -1 if the referenced table row is the first one in the table) - * @pre `iterator` points to an existing table row in the table or it is the end iterator of the table - * @post `*primary` will be replaced with the primary key of the table row preceding the referenced table row if it exists, otherwise `*primary` will be left untouched - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx256_previous(int32_t iterator, uint64_t* primary); - -/** - * @brief Find a table row in a secondary 128-bit integer index table by primary key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param data - Pointer to the an array of 2 `uint128_t` integers which will act as the buffer to hold the retrieved secondary key of the found table row - * @param data_len - Must be set to 2 - * @param primary - The primary key of the table row to look up - * @post If and only if the table row is found, the buffer pointed to by `data` will be filled with the secondary key of the found table row - * @return iterator to the table row with a primary key equal to `id` or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx256_find_primary(capi_name code, uint64_t scope, capi_name table, uint128_t* data, uint32_t data_len, uint64_t primary); - -/** - * @brief Find a table row in a secondary 256-bit index table by secondary key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param data - Pointer to the secondary key data (which is stored as an array of 2 `uint128_t` integers) used to lookup the table row - * @param data_len - Must be set to 2 - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the first table row with a secondary key equal to the specified secondary key or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx256_find_secondary(capi_name code, uint64_t scope, capi_name table, const uint128_t* data, uint32_t data_len, uint64_t* primary); - -/** - * @brief Find the table row in a secondary 256-bit index table that matches the lowerbound condition for a given secondary key - * @details The table row that matches the lowerbound condition is the first table row in the table with the lowest secondary key that is >= the given key (uses lexicographical ordering on the 256-bit keys) - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param data - Pointer to the secondary key data (which is stored as an array of 2 `uint128_t` integers) first used to determine the lowerbound and which is then replaced with the secondary key of the found table row - * @param data_len - Must be set to 2 - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, the buffer pointed to by `data` will be filled with the secondary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the found table row or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx256_lowerbound(capi_name code, uint64_t scope, capi_name table, uint128_t* data, uint32_t data_len, uint64_t* primary); - -/** - * @brief Find the table row in a secondary 256-bit index table that matches the upperbound condition for a given secondary key - * @details The table row that matches the upperbound condition is the first table row in the table with the lowest secondary key that is > the given key (uses lexicographical ordering on the 256-bit keys) - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param data - Pointer to the secondary key data (which is stored as an array of 2 `uint128_t` integers) first used to determine the upperbound and which is then replaced with the secondary key of the found table row - * @param data_len - Must be set to 2 - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, the buffer pointed to by `data` will be filled with the secondary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the found table row or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx256_upperbound(capi_name code, uint64_t scope, capi_name table, uint128_t* data, uint32_t data_len, uint64_t* primary); - -/** - * @brief Get an end iterator representing just-past-the-end of the last table row of a secondary 256-bit index table - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @return end iterator of the table - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx256_end(capi_name code, uint64_t scope, capi_name table); - -/** - * @brief Store an association of a double-precision floating-point secondary key to a primary key in a secondary double-precision floating-point index table - * @param scope - The scope where the table resides (implied to be within the code of the current receiver) - * @param table - The table name - * @param payer - The account that pays for the storage costs - * @param id - The primary key to which to associate the secondary key - * @param secondary - Pointer to the secondary key - * @return iterator to the newly created table row - * @post new secondary key association between primary key `id` and secondary key `*secondary` is created in the secondary double-precision floating-point index table - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_double_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const double* secondary); - -/** - * @brief Update an association for a double-precision floating-point secondary key to a primary key in a secondary double-precision floating-point index table - * @param iterator - The iterator to the table row containing the secondary key association to update - * @param payer - The account that pays for the storage costs (use 0 to continue using current payer) - * @param secondary - Pointer to the **new** secondary key that will replace the existing one of the association - * @pre `iterator` points to an existing table row in the table - * @post the secondary key of the table row pointed to by `iterator` is replaced by `*secondary` - */ -__attribute__((sysio_wasm_import)) -void db_idx_double_update(int32_t iterator, capi_name payer, const double* secondary); - -/** - * @brief Remove a table row from a secondary double-precision floating-point index table - * @param iterator - Iterator to the table row to remove - * @pre `iterator` points to an existing table row in the table - * @post the table row pointed to by `iterator` is removed and the associated storage costs are refunded to the payer - */ -__attribute__((sysio_wasm_import)) -void db_idx_double_remove(int32_t iterator); - -/** - * @brief Find the table row following the referenced table row in a secondary double-precision floating-point index table - * @param iterator - The iterator to the referenced table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the next table row - * @return iterator to the table row following the referenced table row (or the end iterator of the table if the referenced table row is the last one in the table) - * @pre `iterator` points to an existing table row in the table - * @post `*primary` will be replaced with the primary key of the table row following the referenced table row if it exists, otherwise `*primary` will be left untouched - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_double_next(int32_t iterator, uint64_t* primary); - -/** - * @brief Find the table row preceding the referenced table row in a secondary double-precision floating-point index table - * @param iterator - The iterator to the referenced table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the previous table row - * @return iterator to the table row preceding the referenced table row assuming one exists (it will return -1 if the referenced table row is the first one in the table) - * @pre `iterator` points to an existing table row in the table or it is the end iterator of the table - * @post `*primary` will be replaced with the primary key of the table row preceding the referenced table row if it exists, otherwise `*primary` will be left untouched - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_double_previous(int32_t iterator, uint64_t* primary); - -/** - * @brief Find a table row in a secondary double-precision floating-point index table by primary key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to a `double` variable which will have its value set to the secondary key of the found table row - * @param primary - The primary key of the table row to look up - * @post If and only if the table row is found, `*secondary` will be replaced with the secondary key of the found table row - * @return iterator to the table row with a primary key equal to `id` or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_double_find_primary(capi_name code, uint64_t scope, capi_name table, double* secondary, uint64_t primary); - -/** - * @brief Find a table row in a secondary double-precision floating-point index table by secondary key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to secondary key used to lookup the table row - * @param primary - Pointer to a `double` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the first table row with a secondary key equal to `*secondary` or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_double_find_secondary(capi_name code, uint64_t scope, capi_name table, const double* secondary, uint64_t* primary); - -/** - * @brief Find the table row in a secondary double-precision floating-point index table that matches the lowerbound condition for a given secondary key - * @details The table row that matches the lowerbound condition is the first table row in the table with the lowest secondary key that is >= the given key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to secondary key first used to determine the lowerbound and which is then replaced with the secondary key of the found table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*secondary` will be replaced with the secondary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the found table row or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_double_lowerbound(capi_name code, uint64_t scope, capi_name table, double* secondary, uint64_t* primary); - -/** - * @brief Find the table row in a secondary double-precision floating-point index table that matches the upperbound condition for a given secondary key - * @details The table row that matches the upperbound condition is the first table row in the table with the lowest secondary key that is > the given key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to secondary key first used to determine the upperbound and which is then replaced with the secondary key of the found table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*secondary` will be replaced with the secondary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the found table row or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_double_upperbound(capi_name code, uint64_t scope, capi_name table, double* secondary, uint64_t* primary); - -/** - * @brief Get an end iterator representing just-past-the-end of the last table row of a secondary double-precision floating-point index table - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @return end iterator of the table - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_double_end(capi_name code, uint64_t scope, capi_name table); - -/** - * @brief Store an association of a quadruple-precision floating-point secondary key to a primary key in a secondary quadruple-precision floating-point index table - * @param scope - The scope where the table resides (implied to be within the code of the current receiver) - * @param table - The table name - * @param payer - The account that pays for the storage costs - * @param id - The primary key to which to associate the secondary key - * @param secondary - Pointer to the secondary key - * @return iterator to the newly created table row - * @post new secondary key association between primary key `id` and secondary key `*secondary` is created in the secondary quadruple-precision floating-point index table - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_long_double_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const long double* secondary); - -/** - * @brief Update an association for a quadruple-precision floating-point secondary key to a primary key in a secondary quadruple-precision floating-point index table - * @param iterator - The iterator to the table row containing the secondary key association to update - * @param payer - The account that pays for the storage costs (use 0 to continue using current payer) - * @param secondary - Pointer to the **new** secondary key that will replace the existing one of the association - * @pre `iterator` points to an existing table row in the table - * @post the secondary key of the table row pointed to by `iterator` is replaced by `*secondary` - */ -__attribute__((sysio_wasm_import)) -void db_idx_long_double_update(int32_t iterator, capi_name payer, const long double* secondary); - -/** - * @brief Remove a table row from a secondary quadruple-precision floating-point index table - * @param iterator - Iterator to the table row to remove - * @pre `iterator` points to an existing table row in the table - * @post the table row pointed to by `iterator` is removed and the associated storage costs are refunded to the payer - */ -__attribute__((sysio_wasm_import)) -void db_idx_long_double_remove(int32_t iterator); - -/** - * @brief Find the table row following the referenced table row in a secondary quadruple-precision floating-point index table - * @param iterator - The iterator to the referenced table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the next table row - * @return iterator to the table row following the referenced table row (or the end iterator of the table if the referenced table row is the last one in the table) - * @pre `iterator` points to an existing table row in the table - * @post `*primary` will be replaced with the primary key of the table row following the referenced table row if it exists, otherwise `*primary` will be left untouched - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_long_double_next(int32_t iterator, uint64_t* primary); - -/** - * @brief Find the table row preceding the referenced table row in a secondary quadruple-precision floating-point index table - * @param iterator - The iterator to the referenced table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the previous table row - * @return iterator to the table row preceding the referenced table row assuming one exists (it will return -1 if the referenced table row is the first one in the table) - * @pre `iterator` points to an existing table row in the table or it is the end iterator of the table - * @post `*primary` will be replaced with the primary key of the table row preceding the referenced table row if it exists, otherwise `*primary` will be left untouched - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_long_double_previous(int32_t iterator, uint64_t* primary); - -/** - * @brief Find a table row in a secondary quadruple-precision floating-point index table by primary key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to a `long double` variable which will have its value set to the secondary key of the found table row - * @param primary - The primary key of the table row to look up - * @post If and only if the table row is found, `*secondary` will be replaced with the secondary key of the found table row - * @return iterator to the table row with a primary key equal to `id` or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_long_double_find_primary(capi_name code, uint64_t scope, capi_name table, long double* secondary, uint64_t primary); - -/** - * @brief Find a table row in a secondary quadruple-precision floating-point index table by secondary key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to secondary key used to lookup the table row - * @param primary - Pointer to a `long double` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the first table row with a secondary key equal to `*secondary` or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_long_double_find_secondary(capi_name code, uint64_t scope, capi_name table, const long double* secondary, uint64_t* primary); - -/** - * @brief Find the table row in a secondary quadruple-precision floating-point index table that matches the lowerbound condition for a given secondary key - * @details The table row that matches the lowerbound condition is the first table row in the table with the lowest secondary key that is >= the given key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to secondary key first used to determine the lowerbound and which is then replaced with the secondary key of the found table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*secondary` will be replaced with the secondary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the found table row or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_long_double_lowerbound(capi_name code, uint64_t scope, capi_name table, long double* secondary, uint64_t* primary); - -/** - * @brief Find the table row in a secondary quadruple-precision floating-point index table that matches the upperbound condition for a given secondary key - * @details The table row that matches the upperbound condition is the first table row in the table with the lowest secondary key that is > the given key - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @param secondary - Pointer to secondary key first used to determine the upperbound and which is then replaced with the secondary key of the found table row - * @param primary - Pointer to a `uint64_t` variable which will have its value set to the primary key of the found table row - * @post If and only if the table row is found, `*secondary` will be replaced with the secondary key of the found table row - * @post If and only if the table row is found, `*primary` will be replaced with the primary key of the found table row - * @return iterator to the found table row or the end iterator of the table if the table row could not be found - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_long_double_upperbound(capi_name code, uint64_t scope, capi_name table, long double* secondary, uint64_t* primary); - -/** - * @brief Get an end iterator representing just-past-the-end of the last table row of a secondary quadruple-precision floating-point index table - * @param code - The name of the owner of the table - * @param scope - The scope where the table resides - * @param table - The table name - * @return end iterator of the table - */ -__attribute__((sysio_wasm_import)) -int32_t db_idx_long_double_end(capi_name code, uint64_t scope, capi_name table); - -#ifdef __cplusplus -} -#endif -///@} +#include diff --git a/libraries/sysiolib/capi/sysio/kv.h b/libraries/sysiolib/capi/sysio/kv.h new file mode 100644 index 000000000..00cc44c9c --- /dev/null +++ b/libraries/sysiolib/capi/sysio/kv.h @@ -0,0 +1,246 @@ +#pragma once + +#include "types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @defgroup kv_database_c KV Database C API + * @ingroup c_api + * @brief Defines API for KV database intrinsics + * + * Replaces the legacy db_*_i64 intrinsics with a simpler key-value interface. + * Keys and values are arbitrary byte sequences. No scope indirection. + * RAM is charged to the payer account (defaults to the executing contract). + * @{ + */ + +/** + * Store or update a key-value pair. + * + * @param key_format - encoding format: 0 = raw bytes, 1 = standard 24-byte + * [table:8B BE][scope:8B BE][pk:8B BE] layout + * @param payer - account to bill for RAM. Pass 0 to bill the executing contract. + * Non-zero payer requires payer authorization at the transaction level. + * @param key - pointer to key bytes + * @param key_size - length of key in bytes (max 256) + * @param value - pointer to value bytes + * @param value_size - length of value in bytes (max 256 KiB) + * @return RAM byte delta (positive for growth, 0 for same-size update) + */ +__attribute__((sysio_wasm_import)) +int64_t kv_set(uint32_t key_format, uint64_t payer, const void* key, uint32_t key_size, const void* value, uint32_t value_size); + +/** + * Read a value by key. If buffer is too small, returns the actual size without error. + * + * @param key_format - encoding format: 0 = raw bytes, 1 = standard 24-byte + * [table:8B BE][scope:8B BE][pk:8B BE] layout + * @param code - contract account to read from + * @param key - pointer to key bytes + * @param key_size - length of key + * @param value - output buffer + * @param value_size - size of output buffer + * @return actual value size, or -1 if key not found + */ +__attribute__((sysio_wasm_import)) +int32_t kv_get(uint32_t key_format, capi_name code, const void* key, uint32_t key_size, void* value, uint32_t value_size); + +/** + * Erase a key-value pair. Aborts if key not found. + * + * @param key_format - encoding format: 0 = raw bytes, 1 = standard 24-byte layout + * @param key - pointer to key bytes + * @param key_size - length of key + * @return negative RAM byte delta + */ +__attribute__((sysio_wasm_import)) +int64_t kv_erase(uint32_t key_format, const void* key, uint32_t key_size); + +/** + * Check if a key exists. + * + * @param key_format - encoding format: 0 = raw bytes, 1 = standard 24-byte layout + * @param code - contract account to check + * @param key - pointer to key bytes + * @param key_size - length of key + * @return 1 if key exists, 0 otherwise + */ +__attribute__((sysio_wasm_import)) +int32_t kv_contains(uint32_t key_format, capi_name code, const void* key, uint32_t key_size); + +// --- Primary KV Iterators --- + +/** + * Create an iterator over keys matching a prefix for a given contract. + * Positions at the first matching key, or end if none match. + * + * @param key_format - encoding format: 0 = raw bytes, 1 = standard 24-byte layout + * @param code - contract account + * @param prefix - key prefix bytes (empty = iterate all) + * @param prefix_size - length of prefix + * @return iterator handle (0..15) + */ +__attribute__((sysio_wasm_import)) +uint32_t kv_it_create(uint32_t key_format, capi_name code, const void* prefix, uint32_t prefix_size); + +/** + * Destroy an iterator, freeing the slot. + * @param handle - iterator handle + */ +__attribute__((sysio_wasm_import)) +void kv_it_destroy(uint32_t handle); + +/** + * Get iterator status: 0=ok, 1=end, 2=erased. + * @param handle - iterator handle + * @return status code + */ +__attribute__((sysio_wasm_import)) +int32_t kv_it_status(uint32_t handle); + +/** + * Advance iterator to next key within prefix. Returns new status. + * @param handle - iterator handle + * @return status after advancing + */ +__attribute__((sysio_wasm_import)) +int32_t kv_it_next(uint32_t handle); + +/** + * Move iterator to previous key within prefix. Returns new status. + * @param handle - iterator handle + * @return status after retreating + */ +__attribute__((sysio_wasm_import)) +int32_t kv_it_prev(uint32_t handle); + +/** + * Seek iterator to lower bound of key within prefix. Returns new status. + * @param handle - iterator handle + * @param key - seek key bytes + * @param key_size - length of seek key + * @return status after seeking + */ +__attribute__((sysio_wasm_import)) +int32_t kv_it_lower_bound(uint32_t handle, const void* key, uint32_t key_size); + +/** + * Read current iterator's key with offset support. + * @param handle - iterator handle + * @param offset - byte offset into key + * @param dest - output buffer + * @param dest_size - output buffer size + * @param actual_size - [out] actual total key size + * @return status code + */ +__attribute__((sysio_wasm_import)) +int32_t kv_it_key(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size); + +/** + * Read current iterator's value with offset support. + * @param handle - iterator handle + * @param offset - byte offset into value + * @param dest - output buffer + * @param dest_size - output buffer size + * @param actual_size - [out] actual total value size + * @return status code + */ +__attribute__((sysio_wasm_import)) +int32_t kv_it_value(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size); + +// --- Secondary KV Index --- + +/** + * Store a secondary index entry. + * @param payer - account paying for RAM (0 = contract itself) + * @param table - logical table name + * @param index_id - index identifier (0-255) + * @param pri_key - primary key bytes + * @param pri_key_size - primary key size (max 256) + * @param sec_key - secondary key bytes + * @param sec_key_size - secondary key size (max 256) + */ +__attribute__((sysio_wasm_import)) +void kv_idx_store(uint64_t payer, capi_name table, uint32_t index_id, + const void* pri_key, uint32_t pri_key_size, + const void* sec_key, uint32_t sec_key_size); + +/** + * Remove a secondary index entry. + */ +__attribute__((sysio_wasm_import)) +void kv_idx_remove(capi_name table, uint32_t index_id, + const void* pri_key, uint32_t pri_key_size, + const void* sec_key, uint32_t sec_key_size); + +/** + * Update a secondary index entry (change secondary key, keep primary key). + */ +__attribute__((sysio_wasm_import)) +void kv_idx_update(uint64_t payer, capi_name table, uint32_t index_id, + const void* pri_key, uint32_t pri_key_size, + const void* old_sec_key, uint32_t old_sec_key_size, + const void* new_sec_key, uint32_t new_sec_key_size); + +/** + * Find exact secondary key match. + * + * @return iterator handle (>= 0) on match, or -1 if not found. + * When -1 is returned no handle is allocated and kv_idx_destroy + * must NOT be called. + */ +__attribute__((sysio_wasm_import)) +int32_t kv_idx_find_secondary(capi_name code, capi_name table, uint32_t index_id, + const void* sec_key, uint32_t sec_key_size); + +/** + * Find lower bound on secondary key. + * + * @return handle >= 0 positioned at the first entry with sec_key >= bound, + * or handle >= 0 in iterator_end state when bound is past all entries but + * the table is non-empty (enables kv_idx_prev to reach the last entry). + * Returns -1 only when the table has no entries for this (code, table, index_id). + * When -1 is returned no handle is allocated and kv_idx_destroy must NOT be called. + */ +__attribute__((sysio_wasm_import)) +int32_t kv_idx_lower_bound(capi_name code, capi_name table, uint32_t index_id, + const void* sec_key, uint32_t sec_key_size); + +/** + * Advance secondary iterator. Returns status. + */ +__attribute__((sysio_wasm_import)) +int32_t kv_idx_next(uint32_t handle); + +/** + * Retreat secondary iterator. Returns status. + */ +__attribute__((sysio_wasm_import)) +int32_t kv_idx_prev(uint32_t handle); + +/** + * Read current secondary key from iterator. + */ +__attribute__((sysio_wasm_import)) +int32_t kv_idx_key(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size); + +/** + * Read current primary key from secondary iterator. + */ +__attribute__((sysio_wasm_import)) +int32_t kv_idx_primary_key(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size); + +/** + * Destroy a secondary iterator. + */ +__attribute__((sysio_wasm_import)) +void kv_idx_destroy(uint32_t handle); + +/** @} */ + +#ifdef __cplusplus +} +#endif diff --git a/libraries/sysiolib/contracts/sysio/kv_constants.hpp b/libraries/sysiolib/contracts/sysio/kv_constants.hpp new file mode 100644 index 000000000..021c3b9d0 --- /dev/null +++ b/libraries/sysiolib/contracts/sysio/kv_constants.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace sysio::kv { + +/// Key format constants for KV intrinsics. +/// Format determines how the host indexes and partitions key data. +/// Format 0 and format 1 entries are stored in separate index partitions, +/// so there is no collision between raw_table and multi_index/kv::table data. + +/// Raw bytes -- variable-length keys, used by kv::raw_table. +inline constexpr uint32_t kv_format_raw = 0; + +/// Standard 24-byte layout: [table:8B BE][scope:8B BE][pk:8B BE]. +/// Enables SSO fast-path (inline storage + integer comparison) and SHiP +/// translation to legacy contract_row format. +inline constexpr uint32_t kv_format_standard = 1; + +} // namespace sysio::kv diff --git a/libraries/sysiolib/contracts/sysio/kv_multi_index.hpp b/libraries/sysiolib/contracts/sysio/kv_multi_index.hpp new file mode 100644 index 000000000..200cfbafb --- /dev/null +++ b/libraries/sysiolib/contracts/sysio/kv_multi_index.hpp @@ -0,0 +1,1012 @@ +#pragma once +/** + * KV-backed multi_index emulation layer. + * + * Drop-in replacement for sysio::multi_index that uses KV intrinsics instead + * of legacy db_*_i64 intrinsics. Same template API, different backend. + * + * Key encoding: [table_name: 8B BE][scope: 8B BE][primary_key: 8B BE] = 24 bytes + * This fits the SSO fast-path in kv_object (≤24 bytes inline, integer comparison). + * + * The payer parameter is honored — RAM is charged to the specified payer, + * matching the behavior of the legacy sysio::multi_index. + */ + +#include + +// KV intrinsic declarations (primary + secondary — multi_index uses both) +extern "C" { + __attribute__((sysio_wasm_import)) + int64_t kv_set(uint32_t key_format, uint64_t payer, const void* key, uint32_t key_size, const void* value, uint32_t value_size); + __attribute__((sysio_wasm_import)) + int32_t kv_get(uint32_t key_format, uint64_t code, const void* key, uint32_t key_size, void* value, uint32_t value_size); + __attribute__((sysio_wasm_import)) + int64_t kv_erase(uint32_t key_format, const void* key, uint32_t key_size); + __attribute__((sysio_wasm_import)) + int32_t kv_contains(uint32_t key_format, uint64_t code, const void* key, uint32_t key_size); + __attribute__((sysio_wasm_import)) + uint32_t kv_it_create(uint32_t key_format, uint64_t code, const void* prefix, uint32_t prefix_size); + __attribute__((sysio_wasm_import)) + void kv_it_destroy(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_it_status(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_it_next(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_it_prev(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_it_lower_bound(uint32_t handle, const void* key, uint32_t key_size); + __attribute__((sysio_wasm_import)) + int32_t kv_it_key(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size); + __attribute__((sysio_wasm_import)) + int32_t kv_it_value(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size); + __attribute__((sysio_wasm_import)) + void kv_idx_store(uint64_t payer, uint64_t table, uint32_t index_id, + const void* pri_key, uint32_t pri_key_size, + const void* sec_key, uint32_t sec_key_size); + __attribute__((sysio_wasm_import)) + void kv_idx_remove(uint64_t table, uint32_t index_id, + const void* pri_key, uint32_t pri_key_size, + const void* sec_key, uint32_t sec_key_size); + __attribute__((sysio_wasm_import)) + void kv_idx_update(uint64_t payer, uint64_t table, uint32_t index_id, + const void* pri_key, uint32_t pri_key_size, + const void* old_sec_key, uint32_t old_sec_key_size, + const void* new_sec_key, uint32_t new_sec_key_size); + __attribute__((sysio_wasm_import)) + int32_t kv_idx_find_secondary(uint64_t code, uint64_t table, uint32_t index_id, + const void* sec_key, uint32_t sec_key_size); + __attribute__((sysio_wasm_import)) + int32_t kv_idx_lower_bound(uint64_t code, uint64_t table, uint32_t index_id, + const void* sec_key, uint32_t sec_key_size); + __attribute__((sysio_wasm_import)) + int32_t kv_idx_next(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_idx_prev(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_idx_key(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size); + __attribute__((sysio_wasm_import)) + int32_t kv_idx_primary_key(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size); + __attribute__((sysio_wasm_import)) + void kv_idx_destroy(uint32_t handle); +} + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace sysio { + +using sysio::kv::kv_format_raw; +using sysio::kv::kv_format_standard; + +// Type definitions needed by contracts using secondary indices. +// These are pure type templates with no legacy db_* dependencies. +template +struct indexed_by { + enum constants { index_name = static_cast(IndexName) }; + typedef Extractor secondary_extractor_type; +}; + +template +struct const_mem_fun { + typedef typename std::remove_reference::type result_type; + Type operator()(const Class& x) const { return (x.*PtrToMemberFunction)(); } + Type operator()(const Class* x) const { return (x->*PtrToMemberFunction)(); } +}; + +#ifndef SYSIO_SAME_PAYER_DEFINED +#define SYSIO_SAME_PAYER_DEFINED +inline constexpr name same_payer{}; +#endif + +namespace _kv_multi_index_detail { + + inline void encode_be64(char* buf, uint64_t v) { + for (int i = 7; i >= 0; --i) { + buf[i] = static_cast(v & 0xFF); + v >>= 8; + } + } + + inline uint64_t decode_be64(const char* buf) { + uint64_t v = 0; + for (int i = 0; i < 8; ++i) { + v = (v << 8) | static_cast(buf[i]); + } + return v; + } + + // Encode a secondary key to bytes for kv_idx_store + template + inline std::vector encode_secondary(const T& key) { + auto sz = pack_size(key); + std::vector buf(sz); + datastream ds(buf.data(), buf.size()); + ds << key; + return buf; + } + + // Encode uint64_t as 8-byte big-endian (for secondary index) + template<> + inline std::vector encode_secondary(const uint64_t& key) { + std::vector buf(8); + encode_be64(buf.data(), key); + return buf; + } + + // Encode uint128_t as 16-byte big-endian + template<> + inline std::vector encode_secondary(const uint128_t& key) { + std::vector buf(16); + encode_be64(buf.data(), static_cast(key >> 64)); + encode_be64(buf.data() + 8, static_cast(key)); + return buf; + } + + // Encode double as sort-preserving 8 bytes + template<> + inline std::vector encode_secondary(const double& key) { + uint64_t bits; + memcpy(&bits, &key, 8); + // IEEE 754 sign-magnitude → unsigned sortable + if (bits & (uint64_t(1) << 63)) + bits = ~bits; // negative: flip all bits + else + bits ^= (uint64_t(1) << 63); // positive: flip sign bit only + std::vector buf(8); + encode_be64(buf.data(), bits); + return buf; + } + + // Encode long double (128-bit softfloat in WASM) as sort-preserving 16 bytes. + // WASM stores long double as IEEE 754 binary128 in little-endian. + // Convert to big-endian and apply sign-magnitude → unsigned sortable transform. + template<> + inline std::vector encode_secondary(const long double& key) { + char raw[16]; + memcpy(raw, &key, 16); + // Convert little-endian to big-endian + std::vector buf(16); + for (int i = 0; i < 16; ++i) + buf[i] = raw[15 - i]; + // Sign-magnitude to unsigned sortable (sign bit is now buf[0] bit 7) + if (static_cast(buf[0]) & 0x80u) + for (int i = 0; i < 16; ++i) buf[i] = ~buf[i]; // negative: flip all + else + buf[0] = static_cast(static_cast(buf[0]) ^ 0x80u); // positive: flip sign + return buf; + } + +} // namespace _kv_multi_index_detail + +// Uses sysio::indexed_by and sysio::const_mem_fun from the standard CDT headers. +// This class is a drop-in replacement: just change multi_index -> kv_multi_index. + +template +class kv_multi_index { + static_assert(sizeof...(Indices) <= 16, "multi_index supports at most 16 secondary indices"); + + // Helper: convert primary_key() result to uint64_t regardless of return type (uint64_t or name) + static uint64_t to_pk_uint64(uint64_t pk) { return pk; } + static uint64_t to_pk_uint64(name pk) { return pk.value; } + + name _code; + uint64_t _scope; + mutable uint64_t _next_primary_key = 0; + mutable bool _next_primary_key_cached = false; + + // Cached items (deserialized rows) + mutable std::map> _items; + + // --- Key encoding --- + struct primary_key_buf { + char data[24]; + }; + + primary_key_buf make_pk(uint64_t pk) const { + primary_key_buf key; + _kv_multi_index_detail::encode_be64(key.data, static_cast(TableName)); + _kv_multi_index_detail::encode_be64(key.data + 8, _scope); + _kv_multi_index_detail::encode_be64(key.data + 16, pk); + return key; + } + + // 16-byte prefix for iteration (table + scope) + struct prefix_buf { + char data[16]; + }; + + prefix_buf make_prefix() const { + prefix_buf p; + _kv_multi_index_detail::encode_be64(p.data, static_cast(TableName)); + _kv_multi_index_detail::encode_be64(p.data + 8, _scope); + return p; + } + + // 16-byte scoped primary key for secondary index: [scope:8B][pk:8B] + // Scope must be included so that rows with the same primary key in + // different scopes produce distinct secondary-index entries. + struct pk_bytes_buf { + char data[16]; + }; + + pk_bytes_buf pk_to_bytes(uint64_t pk) const { + pk_bytes_buf b; + _kv_multi_index_detail::encode_be64(b.data, _scope); + _kv_multi_index_detail::encode_be64(b.data + 8, pk); + return b; + } + + // --- Row serialization --- + static std::vector serialize_row(const T& obj) { + auto sz = pack_size(obj); + std::vector buf(sz); + datastream ds(buf.data(), buf.size()); + ds << obj; + return buf; + } + + static T deserialize_row(const char* data, size_t size) { + T obj; + datastream ds(data, size); + ds >> obj; + return obj; + } + + T* load_object(uint64_t pk) const { + auto it = _items.find(pk); + if (it != _items.end()) return it->second.get(); + + auto key = make_pk(pk); + int32_t sz = ::kv_get(kv_format_standard, _code.value, key.data, 24, nullptr, 0); + if (sz < 0) return nullptr; + + std::vector buf(sz); + ::kv_get(kv_format_standard, _code.value, key.data, 24, buf.data(), buf.size()); + + auto ptr = std::make_unique(deserialize_row(buf.data(), buf.size())); + auto* raw = ptr.get(); + _items[pk] = std::move(ptr); + return raw; + } + + // --- Secondary index helpers --- + template + struct secondary_ops { + static void store_all(uint64_t payer, const kv_multi_index& idx, const T& obj) { + using extractor_t = typename Index::secondary_extractor_type; + extractor_t ext; + auto sec_key = _kv_multi_index_detail::encode_secondary(ext(obj)); + auto pri_key = idx.pk_to_bytes(obj.primary_key()); + ::kv_idx_store(payer, static_cast(TableName), N, + pri_key.data, 16, + sec_key.data(), sec_key.size()); + if constexpr (sizeof...(Rest) > 0) { + secondary_ops::store_all(payer, idx, obj); + } + } + + static void remove_all(const kv_multi_index& idx, const T& obj) { + using extractor_t = typename Index::secondary_extractor_type; + extractor_t ext; + auto sec_key = _kv_multi_index_detail::encode_secondary(ext(obj)); + auto pri_key = idx.pk_to_bytes(obj.primary_key()); + ::kv_idx_remove(static_cast(TableName), N, + pri_key.data, 16, + sec_key.data(), sec_key.size()); + if constexpr (sizeof...(Rest) > 0) { + secondary_ops::remove_all(idx, obj); + } + } + + static void update_all(uint64_t payer, const kv_multi_index& idx, const T& old_obj, const T& new_obj) { + using extractor_t = typename Index::secondary_extractor_type; + extractor_t ext; + auto old_sec = _kv_multi_index_detail::encode_secondary(ext(old_obj)); + auto new_sec = _kv_multi_index_detail::encode_secondary(ext(new_obj)); + auto pri_key = idx.pk_to_bytes(old_obj.primary_key()); + if (old_sec != new_sec) { + ::kv_idx_update(payer, static_cast(TableName), N, + pri_key.data, 16, + old_sec.data(), old_sec.size(), + new_sec.data(), new_sec.size()); + } + if constexpr (sizeof...(Rest) > 0) { + secondary_ops::update_all(payer, idx, old_obj, new_obj); + } + } + }; + + // Base case — no indices + struct no_secondary_ops { + static void store_all(uint64_t, const kv_multi_index&, const T&) {} + static void remove_all(const kv_multi_index&, const T&) {} + static void update_all(uint64_t, const kv_multi_index&, const T&, const T&) {} + }; + + template + struct sec_ops_selector { using type = secondary_ops<0, Is...>; }; + template<> + struct sec_ops_selector<> { using type = no_secondary_ops; }; + using sec_ops = typename sec_ops_selector::type; + + void store_secondaries(uint64_t payer, const T& obj) const { + sec_ops::store_all(payer, *this, obj); + } + + void remove_secondaries(const T& obj) const { + sec_ops::remove_all(*this, obj); + } + + void update_secondaries(uint64_t payer, const T& old_obj, const T& new_obj) const { + sec_ops::update_all(payer, *this, old_obj, new_obj); + } + +public: + kv_multi_index(name code, uint64_t scope) : _code(code), _scope(scope) {} + + name get_code() const { return _code; } + uint64_t get_scope() const { return _scope; } + + // --- const_iterator --- + struct const_iterator { + using iterator_category = std::bidirectional_iterator_tag; + using value_type = const T; + using difference_type = std::ptrdiff_t; + using pointer = const T*; + using reference = const T&; + + const_iterator() : _idx(nullptr), _handle(-1), _valid(false) {} + + const T& operator*() const { + check(_valid && _obj, "dereferencing invalid iterator"); + return *_obj; + } + + const T* operator->() const { + check(_valid && _obj, "dereferencing invalid iterator"); + return _obj; + } + + const_iterator& operator++() { + check(_idx, "incrementing invalid iterator"); + check(_valid, "cannot increment end iterator"); + if (!_valid) return *this; + int32_t status = ::kv_it_next(_handle); + if (status != 0) { + _valid = false; + _obj = nullptr; + } else { + load_current(); + } + return *this; + } + + // Post-increment/decrement deleted: copy requires kv_it_create + kv_it_lower_bound + // (O(log n) per call). Use ++it / --it instead. + const_iterator operator++(int) = delete; + const_iterator operator--(int) = delete; + + const_iterator& operator--() { + check(_idx, "decrementing invalid iterator"); + if (_handle == -1) { + // End iterator: create a real iterator and seek to last element + auto prefix = _idx->make_prefix(); + _handle = ::kv_it_create(kv_format_standard, _idx->_code.value, prefix.data, 16); + // Seek past the end of this table's prefix range by using a max key + char max_key[24]; + memcpy(max_key, prefix.data, 16); + memset(max_key + 16, 0xFF, 8); + ::kv_it_lower_bound(_handle, max_key, 24); + // Now prev to get the last element + int32_t status = ::kv_it_prev(_handle); + if (status == 0) { + _valid = true; + load_current(); + } else { + _valid = false; + _obj = nullptr; + } + } else { + int32_t status = ::kv_it_prev(_handle); + if (status == 0) { + _valid = true; + load_current(); + } else { + check(false, "cannot decrement iterator at beginning of table"); + } + } + return *this; + } + + friend bool operator==(const const_iterator& a, const const_iterator& b) { + if (!a._valid && !b._valid) return true; // both end + if (!a._valid || !b._valid) return false; + return a._obj && b._obj && a._obj->primary_key() == b._obj->primary_key(); + } + + friend bool operator!=(const const_iterator& a, const const_iterator& b) { + return !(a == b); + } + + ~const_iterator() { + if (_handle != -1) ::kv_it_destroy(_handle); + } + + const_iterator(const_iterator&& o) noexcept + : _idx(o._idx), _handle(o._handle), _valid(o._valid), _obj(o._obj) { + o._handle = -1; + o._valid = false; + o._obj = nullptr; + } + + const_iterator& operator=(const_iterator&& o) noexcept { + if (this != &o) { + if (_handle != -1) ::kv_it_destroy(_handle); + _idx = o._idx; + _handle = o._handle; + _valid = o._valid; + _obj = o._obj; + o._handle = -1; + o._valid = false; + o._obj = nullptr; + } + return *this; + } + + // Copy (creates new iterator handle) + const_iterator(const const_iterator& o) : _idx(o._idx), _valid(o._valid), _obj(o._obj) { + if (o._handle != -1 && _idx) { + auto prefix = _idx->make_prefix(); + _handle = ::kv_it_create(kv_format_standard, _idx->_code.value, prefix.data, 16); + if (_valid && _obj) { + auto key = _idx->make_pk(to_pk_uint64(_obj->primary_key())); + ::kv_it_lower_bound(_handle, key.data, 24); + } + } else { + _handle = -1; + } + } + + const_iterator& operator=(const const_iterator& o) { + if (this != &o) { + if (_handle != -1) ::kv_it_destroy(_handle); + _idx = o._idx; + _valid = o._valid; + _obj = o._obj; + if (o._handle != -1 && _idx) { + auto prefix = _idx->make_prefix(); + _handle = ::kv_it_create(kv_format_standard, _idx->_code.value, prefix.data, 16); + if (_valid && _obj) { + auto key = _idx->make_pk(to_pk_uint64(_obj->primary_key())); + ::kv_it_lower_bound(_handle, key.data, 24); + } + } else { + _handle = -1; + } + } + return *this; + } + + private: + friend class kv_multi_index; + const kv_multi_index* _idx; + int32_t _handle; + bool _valid; + const T* _obj; + + const_iterator(const kv_multi_index* idx, int32_t handle, bool valid) + : _idx(idx), _handle(handle), _valid(valid), _obj(nullptr) { + if (_valid) load_current(); + } + + void load_current() { + if (!_valid || _handle < 0) { _obj = nullptr; return; } + // Read the primary key from the KV key (last 8 bytes of 24-byte key) + char key_buf[24]; + uint32_t actual_size = 0; + int32_t status = ::kv_it_key(_handle, 0, key_buf, 24, &actual_size); + if (status != 0 || actual_size != 24) { + _valid = false; + _obj = nullptr; + return; + } + uint64_t pk = _kv_multi_index_detail::decode_be64(key_buf + 16); + _obj = _idx->load_object(pk); + if (!_obj) { + _valid = false; + } + } + }; + + // --- Primary key operations --- + + const_iterator begin() const { + auto prefix = make_prefix(); + uint32_t handle = ::kv_it_create(kv_format_standard, _code.value, prefix.data, 16); + int32_t status = ::kv_it_status(handle); + return const_iterator(this, handle, status == 0); + } + + const_iterator end() const { + return const_iterator(this, -1, false); + } + + const_iterator cbegin() const { return begin(); } + const_iterator cend() const { return end(); } + + using const_reverse_iterator = std::reverse_iterator; + const_reverse_iterator rbegin() const { return const_reverse_iterator(end()); } + const_reverse_iterator rend() const { return const_reverse_iterator(begin()); } + const_reverse_iterator crbegin() const { return rbegin(); } + const_reverse_iterator crend() const { return rend(); } + + const_iterator find(name primary) const { return find(primary.value); } + const_iterator find(uint64_t primary) const { + auto key = make_pk(primary); + int32_t sz = ::kv_get(kv_format_standard, _code.value, key.data, 24, nullptr, 0); + if (sz < 0) return end(); + + // Create iterator positioned at this key + auto prefix = make_prefix(); + uint32_t handle = ::kv_it_create(kv_format_standard, _code.value, prefix.data, 16); + ::kv_it_lower_bound(handle, key.data, 24); + return const_iterator(this, handle, true); + } + + const_iterator require_find(name primary, const char* error_msg = "unable to find key") const { return require_find(primary.value, error_msg); } + const_iterator require_find(uint64_t primary, const char* error_msg = "unable to find key") const { + auto itr = find(primary); + check(itr != end(), error_msg); + return itr; + } + + const T& get(name primary, const char* error_msg = "unable to find key") const { return get(primary.value, error_msg); } + const T& get(uint64_t primary, const char* error_msg = "unable to find key") const { + auto* obj = load_object(primary); + check(obj != nullptr, error_msg); + return *obj; + } + + const_iterator lower_bound(uint64_t primary) const { + auto key = make_pk(primary); + auto prefix = make_prefix(); + uint32_t handle = ::kv_it_create(kv_format_standard, _code.value, prefix.data, 16); + int32_t status = ::kv_it_lower_bound(handle, key.data, 24); + return const_iterator(this, handle, status == 0); + } + + const_iterator upper_bound(uint64_t primary) const { + if (primary == std::numeric_limits::max()) return end(); + return lower_bound(primary + 1); + } + + const_iterator iterator_to(const T& obj) const { + uint64_t pk = to_pk_uint64(obj.primary_key()); + check(_items.find(pk) != _items.end(), + "object passed to iterator_to is not in multi_index"); + auto key = make_pk(pk); + auto prefix = make_prefix(); + uint32_t handle = ::kv_it_create(kv_format_standard, _code.value, prefix.data, 16); + ::kv_it_lower_bound(handle, key.data, 24); + return const_iterator(this, handle, true); + } + + // --- Mutation --- + + template + const_iterator emplace(name payer, Lambda&& constructor) { + T obj; + constructor(obj); + + uint64_t pk = to_pk_uint64(obj.primary_key()); + auto key = make_pk(pk); + auto value = serialize_row(obj); + + ::kv_set(1, payer.value, key.data, 24, value.data(), value.size()); + store_secondaries(payer.value, obj); + + // Cache the object + auto ptr = std::make_unique(std::move(obj)); + _items[pk] = std::move(ptr); + + // Update auto-increment + if (_next_primary_key_cached && pk >= _next_primary_key) { + check(pk < std::numeric_limits::max(), + "next primary key in table is at autoincrement limit"); + _next_primary_key = pk + 1; + } + + return find(pk); + } + + template + void modify(const const_iterator& itr, name payer, Lambda&& updater) { + check(itr != end(), "cannot pass end iterator to modify"); + modify(*itr, payer, std::forward(updater)); + } + + template + void modify(const T& obj, name payer, Lambda&& updater) { + T old_obj = obj; + // Cast away const for modification (same pattern as legacy multi_index) + auto& mutable_obj = const_cast(obj); + updater(mutable_obj); + check(mutable_obj.primary_key() == old_obj.primary_key(), "updater cannot change primary key when modifying an object"); + + uint64_t pk = to_pk_uint64(mutable_obj.primary_key()); + auto key = make_pk(pk); + auto value = serialize_row(mutable_obj); + ::kv_set(1, payer.value, key.data, 24, value.data(), value.size()); + + update_secondaries(payer.value, old_obj, mutable_obj); + + // Update cache + _items[pk] = std::make_unique(mutable_obj); + } + + const_iterator erase(const_iterator itr) { + check(itr != end(), "cannot pass end iterator to erase"); + auto next = itr; + ++next; + + erase(*itr); + return next; + } + + void erase(const T& obj) { + uint64_t pk = to_pk_uint64(obj.primary_key()); + auto key = make_pk(pk); + + remove_secondaries(obj); + ::kv_erase(kv_format_standard, key.data, 24); + _items.erase(pk); + } + + uint64_t available_primary_key() const { + if (_next_primary_key_cached) return _next_primary_key; + + _next_primary_key_cached = true; + + // Find the last key in the table + auto prefix = make_prefix(); + uint32_t handle = ::kv_it_create(kv_format_standard, _code.value, prefix.data, 16); + int32_t status = ::kv_it_status(handle); + + if (status != 0) { + // Empty table + ::kv_it_destroy(handle); + _next_primary_key = 0; + return 0; + } + + ::kv_it_destroy(handle); + + // Create iterator and seek past all possible primary keys + handle = ::kv_it_create(kv_format_standard, _code.value, prefix.data, 16); + char max_key[24]; + _kv_multi_index_detail::encode_be64(max_key, static_cast(TableName)); + _kv_multi_index_detail::encode_be64(max_key + 8, _scope); + _kv_multi_index_detail::encode_be64(max_key + 16, std::numeric_limits::max()); + ::kv_it_lower_bound(handle, max_key, 24); + + // Check if we found max key exactly + status = ::kv_it_status(handle); + if (status == 0) { + // The max key exists — can't auto-increment past max + ::kv_it_destroy(handle); + _next_primary_key = 0; + _next_primary_key_cached = false; + check(false, "next primary key in table is at autoincrement limit"); + return 0; + } + + // Go back to find the actual last key + status = ::kv_it_prev(handle); + if (status != 0) { + // Empty table + ::kv_it_destroy(handle); + _next_primary_key = 0; + return 0; + } + + char key_buf[24]; + uint32_t actual_size = 0; + ::kv_it_key(handle, 0, key_buf, 24, &actual_size); + ::kv_it_destroy(handle); + + if (actual_size == 24) { + uint64_t last_pk = _kv_multi_index_detail::decode_be64(key_buf + 16); + check(last_pk < std::numeric_limits::max(), + "next primary key in table is at autoincrement limit"); + _next_primary_key = last_pk + 1; + } else { + _next_primary_key = 0; + } + + return _next_primary_key; + } + + // --- Secondary index access --- + + template + struct secondary_index_view { + template + struct find_index_number { + static constexpr size_t value = + (static_cast(First::index_name) == static_cast(IndexName)) + ? N : find_index_number::value; + }; + template + struct find_index_number { + static constexpr size_t value = + (static_cast(Last::index_name) == static_cast(IndexName)) + ? N : N + 1; + }; + + static constexpr size_t index_number = find_index_number<0, Indices...>::value; + static_assert(index_number < sizeof...(Indices), "invalid secondary index name"); + + using index_type = typename std::tuple_element>::type; + using secondary_extractor_type = typename index_type::secondary_extractor_type; + using secondary_key_type = std::decay_t; + + const kv_multi_index* _mi; + secondary_index_view(const kv_multi_index& mi) : _mi(&mi) {} + + // Helper: read scoped primary key from secondary iterator handle. + // The stored pri_key is [scope:8B][pk:8B] = 16 bytes; we extract the pk. + static bool read_primary_key(uint32_t handle, uint64_t& pk) { + char pri_buf[16]; + uint32_t actual = 0; + int32_t status = ::kv_idx_primary_key(handle, 0, pri_buf, 16, &actual); + if (status != 0 || actual != 16) return false; + pk = _kv_multi_index_detail::decode_be64(pri_buf + 8); + return true; + } + + // Secondary const_iterator + struct const_iterator { + using iterator_category = std::bidirectional_iterator_tag; + using value_type = const T; + using difference_type = std::ptrdiff_t; + using pointer = const T*; + using reference = const T&; + + const_iterator() = default; + + const T& operator*() const { check(_has_obj, "deref invalid sec iter"); return _obj; } + const T* operator->() const { check(_has_obj, "deref invalid sec iter"); return &_obj; } + + const_iterator& operator++() { + check(_has_obj, "cannot increment end iterator"); + if (!_has_obj || _handle < 0) return *this; + if (::kv_idx_next(_handle) == 0) { load_current(); } + else { _has_obj = false; } + return *this; + } + // Post-increment/decrement deleted: copy requires host function calls. Use ++it / --it. + const_iterator operator++(int) = delete; + const_iterator operator--(int) = delete; + + const_iterator& operator--() { + if (_handle < 0) { + // End sentinel: create real iterator at last entry. + // Use lower_bound with a maximal key (all 0xFF) to position + // past all entries, then prev to land on the last one. + // 1024 = max configurable secondary key size (on-chain param). + char max_sec[1024]; + memset(max_sec, 0xFF, sizeof(max_sec)); + _handle = ::kv_idx_lower_bound( + _mi->_code.value, static_cast(TableName), index_number, + max_sec, sizeof(max_sec)); + if (_handle < 0) { _has_obj = false; } + else if (::kv_idx_prev(_handle) == 0) { _has_obj = true; load_current(); } + else { _has_obj = false; } + } else { + if (::kv_idx_prev(_handle) == 0) { _has_obj = true; load_current(); } + else { check(false, "cannot decrement iterator at beginning of index"); } + } + return *this; + } + friend bool operator==(const const_iterator& a, const const_iterator& b) { + if (!a._has_obj && !b._has_obj) return true; + if (!a._has_obj || !b._has_obj) return false; + return a._pk == b._pk; + } + friend bool operator!=(const const_iterator& a, const const_iterator& b) { return !(a == b); } + + ~const_iterator() { if (_handle >= 0) ::kv_idx_destroy(_handle); } + const_iterator(const_iterator&& o) noexcept + : _mi(o._mi), _handle(o._handle), _has_obj(o._has_obj), _pk(o._pk), _obj(std::move(o._obj)) + { o._handle = -1; o._has_obj = false; } + const_iterator& operator=(const_iterator&& o) noexcept { + if (this != &o) { + if (_handle >= 0) ::kv_idx_destroy(_handle); + _mi = o._mi; _handle = o._handle; _has_obj = o._has_obj; _pk = o._pk; _obj = std::move(o._obj); + o._handle = -1; o._has_obj = false; + } + return *this; + } + const_iterator(const const_iterator& o) : _mi(o._mi), _has_obj(o._has_obj), _pk(o._pk), _obj(o._obj) { + _clone_handle(o); + } + const_iterator& operator=(const const_iterator& o) { + if (this != &o) { + if (_handle >= 0) ::kv_idx_destroy(_handle); + _mi = o._mi; _has_obj = o._has_obj; _pk = o._pk; _obj = o._obj; + _clone_handle(o); + } + return *this; + } + + private: + void _clone_handle(const const_iterator& o) { + if (o._handle >= 0 && _mi && _has_obj) { + using extractor_t = typename std::tuple_element>::type; + extractor_t ext; + auto sec_bytes = _kv_multi_index_detail::encode_secondary(ext(_obj)); + _handle = ::kv_idx_find_secondary( + _mi->_code.value, static_cast(TableName), index_number, + sec_bytes.data(), sec_bytes.size()); + if (_handle < 0) return; + // kv_idx_find_secondary lands on the first row with this + // secondary key, but the source iterator may point to a later + // duplicate. Advance until we find the matching primary key. + uint64_t found_pk = 0; + if (read_primary_key(_handle, found_pk) && found_pk == _pk) + return; + while (::kv_idx_next(_handle) == 0) { + if (read_primary_key(_handle, found_pk) && found_pk == _pk) + return; + } + } else { + _handle = -1; + } + } + + friend struct secondary_index_view; + const kv_multi_index* _mi = nullptr; + int32_t _handle = -1; + bool _has_obj = false; + uint64_t _pk = 0; + T _obj; + + const_iterator(const kv_multi_index* mi, int32_t handle, bool valid) + : _mi(mi), _handle(handle), _has_obj(valid) { + if (_has_obj) load_current(); + } + static const_iterator make_end(const kv_multi_index* mi) { + const_iterator it; it._mi = mi; return it; + } + + void load_current() { + if (_handle < 0) { _has_obj = false; return; } + if (!read_primary_key(_handle, _pk)) { _has_obj = false; return; } + auto* ptr = _mi->load_object(_pk); + if (ptr) { _obj = *ptr; _has_obj = true; } + else { _has_obj = false; } + } + }; + + const_iterator end() const { return const_iterator::make_end(_mi); } + const_iterator cend() const { return end(); } + const_iterator cbegin() const { return begin(); } + + using const_reverse_iterator = std::reverse_iterator; + const_reverse_iterator rbegin() const { return const_reverse_iterator(end()); } + const_reverse_iterator rend() const { return const_reverse_iterator(begin()); } + const_reverse_iterator crbegin() const { return rbegin(); } + const_reverse_iterator crend() const { return rend(); } + + const_iterator begin() const { + // Lower bound with empty key = first entry + int32_t handle = ::kv_idx_lower_bound( + _mi->_code.value, static_cast(TableName), index_number, nullptr, 0); + if (handle < 0) return end(); + return const_iterator(_mi, handle, true); + } + + template + const_iterator find(const SecKey& sec_key) const { + auto sec_bytes = _kv_multi_index_detail::encode_secondary(secondary_key_type(sec_key)); + int32_t handle = ::kv_idx_find_secondary( + _mi->_code.value, static_cast(TableName), index_number, + sec_bytes.data(), sec_bytes.size()); + if (handle < 0) return end(); + return const_iterator(_mi, handle, true); + } + + template + const_iterator lower_bound(const SecKey& sec_key) const { + auto sec_bytes = _kv_multi_index_detail::encode_secondary(secondary_key_type(sec_key)); + int32_t handle = ::kv_idx_lower_bound( + _mi->_code.value, static_cast(TableName), index_number, + sec_bytes.data(), sec_bytes.size()); + if (handle < 0) return end(); + return const_iterator(_mi, handle, true); + } + + template + const_iterator require_find(const SecKey& sec_key, const char* msg = "unable to find secondary key") const { + auto itr = find(sec_key); + check(itr != end(), msg); + return itr; + } + + template + const_iterator upper_bound(const SecKey& sec_key) const { + // upper_bound = lower_bound then skip entries matching the key + using extractor_t = typename std::tuple_element>::type; + extractor_t ext; + auto target_sec = _kv_multi_index_detail::encode_secondary(secondary_key_type(sec_key)); + auto itr = lower_bound(sec_key); + while (itr != end()) { + auto itr_sec = _kv_multi_index_detail::encode_secondary(ext(*itr)); + if (itr_sec != target_sec) break; + ++itr; + } + return itr; + } + + template + const T& get(const SecKey& sec_key, const char* msg = "unable to find secondary key") const { + auto itr = find(sec_key); + check(itr != end(), msg); + return *itr; + } + + // Modify the primary row referenced by a secondary iterator + template + void modify(const const_iterator& itr, name payer, Lambda&& updater) { + check(itr._has_obj, "cannot pass end iterator to modify"); + const_cast(_mi)->modify(itr._obj, payer, std::forward(updater)); + } + + // Find the secondary iterator for a given row object. + // When multiple rows share the same secondary key, advance + // to the entry matching this object's primary key. + const_iterator iterator_to(const T& obj) { + using extractor_t = typename std::tuple_element>::type; + extractor_t ext; + auto target_pk = to_pk_uint64(obj.primary_key()); + auto it = find(ext(obj)); + while (it != end() && it._pk != target_pk) { + ++it; + } + return it; + } + + // Erase the primary row referenced by a secondary iterator, returns next iterator + const_iterator erase(const const_iterator& itr) { + check(itr._has_obj, "cannot pass end iterator to erase"); + auto next = itr; + ++next; + const_cast(_mi)->erase(itr._obj); + return next; + } + + name get_code() const { return name(_mi->_code.value); } + uint64_t get_scope() const { return _mi->_scope; } + }; + + template + secondary_index_view get_index() const { + return secondary_index_view(*this); + } +}; + +} // namespace sysio diff --git a/libraries/sysiolib/contracts/sysio/kv_raw_table.hpp b/libraries/sysiolib/contracts/sysio/kv_raw_table.hpp new file mode 100644 index 000000000..b1bddac7c --- /dev/null +++ b/libraries/sysiolib/contracts/sysio/kv_raw_table.hpp @@ -0,0 +1,389 @@ +#pragma once + +#include +#include +#include +#include + +// KV intrinsic declarations +extern "C" { + __attribute__((sysio_wasm_import)) + int64_t kv_set(uint32_t key_format, uint64_t payer, const void* key, uint32_t key_size, const void* value, uint32_t value_size); + __attribute__((sysio_wasm_import)) + int32_t kv_get(uint32_t key_format, uint64_t code, const void* key, uint32_t key_size, void* value, uint32_t value_size); + __attribute__((sysio_wasm_import)) + int64_t kv_erase(uint32_t key_format, const void* key, uint32_t key_size); + __attribute__((sysio_wasm_import)) + int32_t kv_contains(uint32_t key_format, uint64_t code, const void* key, uint32_t key_size); + __attribute__((sysio_wasm_import)) + uint32_t kv_it_create(uint32_t key_format, uint64_t code, const void* prefix, uint32_t prefix_size); + __attribute__((sysio_wasm_import)) + void kv_it_destroy(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_it_status(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_it_next(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_it_prev(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_it_lower_bound(uint32_t handle, const void* key, uint32_t key_size); + __attribute__((sysio_wasm_import)) + int32_t kv_it_key(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size); + __attribute__((sysio_wasm_import)) + int32_t kv_it_value(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size); +} +#include +#include +#include +#include + +#include + +namespace sysio { namespace kv { + +/** + * Big-endian key encoder for ordered KV storage. + * + * Writes each field in big-endian byte order so that memcmp-based key + * comparison produces correct lexicographic ordering. + * + * Variable-length fields (string, vector) use NUL-escape encoding: + * 0x00 -> 0x00 0x01, terminated by 0x00 0x00 + * This preserves sort order for arbitrary byte sequences including embedded NULs. + * + * Works with SYSLIB_SERIALIZE-generated operators: `stream << key` + * calls operator<< for each field, routing to BE encoding automatically. + */ +class be_key_stream { + std::vector buf_; + + void write_be32(uint32_t v) { + size_t off = buf_.size(); + buf_.resize(off + 4); + for (int i = 3; i >= 0; --i) { buf_[off + i] = static_cast(v & 0xFF); v >>= 8; } + } + + void write_be64(uint64_t v) { + size_t off = buf_.size(); + buf_.resize(off + 8); + for (int i = 7; i >= 0; --i) { buf_[off + i] = static_cast(v & 0xFF); v >>= 8; } + } + + // NUL-escape encoding: 0x00 -> 0x00,0x01 + 0x00,0x00 terminator. + // Preserves lexicographic ordering for arbitrary byte sequences. + void write_escaped(const char* data, size_t len) { + for (size_t i = 0; i < len; ++i) { + buf_.push_back(data[i]); + if (data[i] == '\0') buf_.push_back('\x01'); + } + buf_.push_back('\0'); + buf_.push_back('\0'); + } + +public: + void write(const char* data, size_t len) { + if (len > 0) buf_.insert(buf_.end(), data, data + len); + } + + be_key_stream& operator<<(uint8_t v) { buf_.push_back(static_cast(v)); return *this; } + be_key_stream& operator<<(int8_t v) { return *this << static_cast(static_cast(v) ^ 0x80u); } + + be_key_stream& operator<<(uint16_t v) { + buf_.push_back(static_cast((v >> 8) & 0xFF)); + buf_.push_back(static_cast(v & 0xFF)); + return *this; + } + be_key_stream& operator<<(int16_t v) { return *this << static_cast(static_cast(v) ^ 0x8000u); } + + be_key_stream& operator<<(uint32_t v) { write_be32(v); return *this; } + be_key_stream& operator<<(int32_t v) { return *this << static_cast(static_cast(v) ^ 0x80000000u); } + + be_key_stream& operator<<(uint64_t v) { write_be64(v); return *this; } + be_key_stream& operator<<(int64_t v) { + return *this << static_cast(static_cast(v) ^ (uint64_t(1) << 63)); + } + + be_key_stream& operator<<(uint128_t v) { + write_be64(static_cast(v >> 64)); + write_be64(static_cast(v)); + return *this; + } + be_key_stream& operator<<(int128_t v) { + uint128_t bits = static_cast(v) ^ (uint128_t(1) << 127); + write_be64(static_cast(bits >> 64)); + write_be64(static_cast(bits)); + return *this; + } + + be_key_stream& operator<<(const name& v) { write_be64(v.value); return *this; } + + be_key_stream& operator<<(float v) { + uint32_t bits; + std::memcpy(&bits, &v, 4); + if (bits >> 31) bits = ~bits; + else bits ^= (uint32_t(1) << 31); + write_be32(bits); + return *this; + } + + be_key_stream& operator<<(double v) { + uint64_t bits; + std::memcpy(&bits, &v, 8); + if (bits >> 63) bits = ~bits; + else bits ^= (uint64_t(1) << 63); + write_be64(bits); + return *this; + } + + be_key_stream& operator<<(bool v) { buf_.push_back(v ? 1 : 0); return *this; } + + be_key_stream& operator<<(const std::string& v) { + write_escaped(v.data(), v.size()); + return *this; + } + + be_key_stream& operator<<(const std::vector& v) { + write_escaped(v.data(), v.size()); + return *this; + } + + std::vector release() { return std::move(buf_); } + const char* data() const { return buf_.data(); } + size_t size() const { return buf_.size(); } +}; + +/** + * kv::raw_table -- Low-level ordered key-value storage using format=0 raw keys. + * + * WARNING: All raw_table instances on the same contract share a SINGLE FLAT + * NAMESPACE (all format=0 entries). If your contract has multiple raw_tables, + * you MUST add a discriminator field to your key struct to prevent collisions: + * + * struct user_key { + * uint8_t tag = 1; // unique per raw_table instance + * uint64_t id; + * SYSLIB_SERIALIZE(user_key, (tag)(id)) + * }; + * + * Format=0 entries are stored separately from format=1 entries used by + * kv_multi_index and kv::table, so there is no collision between raw_table + * and multi_index/kv::table data. + * + * Keys are big-endian encoded for correct memcmp ordering. Values use standard + * ABI serialization (little-endian) so SHiP clients can decode them via ABI. + * + * Performance tip: Keys <= 24 bytes benefit from the SSO (Small String + * Optimization) fast-path in the host. The host stores keys <= 24 bytes + * inline (no heap allocation) and uses integer comparison instead of memcmp. + * If your key struct is <= 24 bytes, consider aligning fields for this path. + * + * Usage: + * struct my_key { + * std::string region; + * uint64_t id; + * SYSLIB_SERIALIZE(my_key, (region)(id)) + * }; + * struct [[sysio::table("mydata"), sysio::kv_key("my_key")]] my_value { + * std::string payload; + * uint64_t amount; + * SYSLIB_SERIALIZE(my_value, (payload)(amount)) + * }; + * + * kv::raw_table store; + * store.set({"us-east", 1}, {"hello", 100}); + * auto val = store.get({"us-east", 1}); + * for (auto it = store.begin(); it != store.end(); ++it) { ... } + */ +template +class raw_table { + static std::vector make_key(const K& key) { + be_key_stream bs; + bs << key; + return bs.release(); + } + + static std::vector serialize_value(const V& value) { + auto sz = sysio::pack_size(value); + std::vector buf(sz); + sysio::datastream ds(buf.data(), buf.size()); + ds << value; + return buf; + } + + static V deserialize_value(const char* data, size_t size) { + V value; + sysio::datastream ds(data, size); + ds >> value; + return value; + } + + uint64_t _code = 0; + + uint64_t code() const { return _code ? _code : sysio::current_receiver().value; } + +public: + /// Construct a raw_table. Reads/iterates against \p code's KV data (default: current contract). + raw_table(sysio::name code = sysio::name{}) : _code(code.value) {} + + // --- Point operations --- + + int64_t set(const K& key, const V& value) { + auto k = make_key(key); + auto v = serialize_value(value); + return ::kv_set(kv_format_raw, 0, k.data(), k.size(), v.data(), v.size()); + } + + std::optional get(const K& key) const { + auto k = make_key(key); + std::optional result; + + int32_t sz = ::kv_get(kv_format_raw, code(), k.data(), k.size(), nullptr, 0); + if (sz < 0) return result; + + std::vector buf(sz); + ::kv_get(kv_format_raw, code(), k.data(), k.size(), buf.data(), buf.size()); + result.emplace(deserialize_value(buf.data(), buf.size())); + return result; + } + + bool contains(const K& key) const { + auto k = make_key(key); + return ::kv_contains(kv_format_raw, code(), k.data(), k.size()) != 0; + } + + int64_t erase(const K& key) { + auto k = make_key(key); + return ::kv_erase(kv_format_raw, k.data(), k.size()); + } + + // --- Ordered iteration --- + + struct const_iterator { + using iterator_category = std::bidirectional_iterator_tag; + using value_type = V; + using difference_type = std::ptrdiff_t; + using pointer = const V*; + using reference = const V&; + + const_iterator() = default; + + const V& operator*() const { sysio::check(_valid, "deref end iterator"); return _val; } + const V* operator->() const { sysio::check(_valid, "deref end iterator"); return &_val; } + + const_iterator& operator++() { + if (!_valid) return *this; + if (::kv_it_next(_handle) != 0) _valid = false; + else load(); + return *this; + } + // Post-increment/decrement deleted: copy requires host function calls. Use ++it / --it. + const_iterator operator++(int) = delete; + const_iterator operator--(int) = delete; + + const_iterator& operator--() { + if (_handle < 0) { + // End sentinel: create handle and seek past all entries. + // Use a maximal key (all 0xFF) to position past the last entry, + // then prev to land on it. 1024 = max configurable key size (on-chain param). + ensure_handle(); + char max_key[1024]; + memset(max_key, 0xFF, sizeof(max_key)); + ::kv_it_lower_bound(_handle, max_key, sizeof(max_key)); + if (::kv_it_prev(_handle) == 0) { _valid = true; load(); } + else _valid = false; + } else { + if (::kv_it_prev(_handle) == 0) { _valid = true; load(); } + else _valid = false; + } + return *this; + } + friend bool operator==(const const_iterator& a, const const_iterator& b) { + return a._valid == b._valid && (!a._valid || a._raw_key == b._raw_key); + } + friend bool operator!=(const const_iterator& a, const const_iterator& b) { return !(a == b); } + + ~const_iterator() { if (_handle >= 0) ::kv_it_destroy(_handle); } + + const_iterator(const_iterator&& o) noexcept + : _tbl(o._tbl), _handle(o._handle), _valid(o._valid), + _val(std::move(o._val)), _raw_key(std::move(o._raw_key)) + { o._handle = -1; o._valid = false; } + + const_iterator& operator=(const_iterator&& o) noexcept { + if (this != &o) { + if (_handle >= 0) ::kv_it_destroy(_handle); + _tbl = o._tbl; _handle = o._handle; _valid = o._valid; + _val = std::move(o._val); _raw_key = std::move(o._raw_key); + o._handle = -1; o._valid = false; + } + return *this; + } + const_iterator(const const_iterator&) = delete; + const_iterator& operator=(const const_iterator&) = delete; + + private: + friend class raw_table; + const raw_table* _tbl = nullptr; + int32_t _handle = -1; + bool _valid = false; + V _val{}; + std::vector _raw_key; + + const_iterator(const raw_table* t, int32_t h, bool valid) + : _tbl(t), _handle(h), _valid(valid) { + if (_valid) load(); + } + static const_iterator make_end(const raw_table* m) { const_iterator it; it._tbl = m; return it; } + + void load() { + // Read key for equality comparison + uint32_t key_size = 0; + ::kv_it_key(_handle, 0, nullptr, 0, &key_size); + _raw_key.resize(key_size); + if (::kv_it_key(_handle, 0, _raw_key.data(), key_size, &key_size) != 0) { + _valid = false; return; + } + // Read and deserialize value + uint32_t val_size = 0; + ::kv_it_value(_handle, 0, nullptr, 0, &val_size); + std::vector vbuf(val_size); + ::kv_it_value(_handle, 0, vbuf.data(), val_size, &val_size); + _val = deserialize_value(vbuf.data(), vbuf.size()); + } + + void ensure_handle() { + if (_handle >= 0) return; + if (!_tbl) return; + // Empty prefix: iterate all format=0 entries in this contract + _handle = ::kv_it_create(kv_format_raw, _tbl->code(), nullptr, 0); + if (_valid && !_raw_key.empty()) { + ::kv_it_lower_bound(_handle, _raw_key.data(), _raw_key.size()); + } + } + }; + + const_iterator end() const { return const_iterator::make_end(this); } + + const_iterator begin() const { + // Empty prefix: iterate all format=0 entries + uint32_t h = ::kv_it_create(kv_format_raw, code(), nullptr, 0); + return const_iterator(this, h, ::kv_it_status(h) == 0); + } + + /// First entry with key >= given key. + const_iterator lower_bound(const K& key) const { + auto full = make_key(key); + uint32_t h = ::kv_it_create(kv_format_raw, code(), nullptr, 0); + int32_t status = ::kv_it_lower_bound(h, full.data(), full.size()); + return const_iterator(this, h, status == 0); + } + + /// First entry with key > given key. + const_iterator upper_bound(const K& key) const { + auto it = lower_bound(key); + if (it != end() && it._raw_key == make_key(key)) ++it; + return it; + } +}; + +} } // namespace sysio::kv diff --git a/libraries/sysiolib/contracts/sysio/kv_singleton.hpp b/libraries/sysiolib/contracts/sysio/kv_singleton.hpp new file mode 100644 index 000000000..2a641fd40 --- /dev/null +++ b/libraries/sysiolib/contracts/sysio/kv_singleton.hpp @@ -0,0 +1,70 @@ +#pragma once +#include "kv_table.hpp" +#include "system.hpp" + +namespace sysio { + + /** + * KV-backed singleton. Drop-in replacement for sysio::singleton. + * Uses sysio::kv::table instead of legacy multi_index. + */ + template + class kv_singleton + { + constexpr static uint64_t pk_value = static_cast(SingletonName); + + struct row { + T value; + uint64_t primary_key() const { return pk_value; } + SYSLIB_SERIALIZE( row, (value) ) + }; + + typedef sysio::kv::table table; + + public: + + kv_singleton( name code, uint64_t scope ) : _t( code, scope ) {} + + bool exists() const { + return _t.find( pk_value ) != _t.end(); + } + + T get() const { + auto itr = _t.find( pk_value ); + sysio::check( itr != _t.end(), "singleton does not exist" ); + return itr->value; + } + + T get_or_default( const T& def = T() ) const { + auto itr = _t.find( pk_value ); + return itr != _t.end() ? itr->value : def; + } + + T get_or_create( name bill_to_account, const T& def = T() ) { + auto itr = _t.find( pk_value ); + if (itr != _t.end()) return itr->value; + _t.emplace(bill_to_account, [&](row& r) { r.value = def; }); + return def; + } + + void set( const T& value, name bill_to_account ) { + auto itr = _t.find( pk_value ); + if( itr != _t.end() ) { + _t.modify(itr, bill_to_account, [&](row& r) { r.value = value; }); + } else { + _t.emplace(bill_to_account, [&](row& r) { r.value = value; }); + } + } + + void remove( ) { + auto itr = _t.find( pk_value ); + if( itr != _t.end() ) { + _t.erase(itr); + } + } + + private: + table _t; + }; + +} /// namespace sysio diff --git a/libraries/sysiolib/contracts/sysio/kv_table.hpp b/libraries/sysiolib/contracts/sysio/kv_table.hpp new file mode 100644 index 000000000..6ce9046dd --- /dev/null +++ b/libraries/sysiolib/contracts/sysio/kv_table.hpp @@ -0,0 +1,468 @@ +#pragma once +/** + * sysio::kv::table — High-performance KV-backed table with multi_index-like API. + * + * Automatically selects zero-copy storage for trivially_copyable structs (memcpy, + * no pack/unpack) or falls back to datastream serialization for complex types. + * + * Key encoding: [table:8B BE][scope:8B BE][pk:8B BE] = 24 bytes (SSO fast-path). + * SHiP compatible: keys can be reverse-mapped to legacy contract_row format. + */ + +// KV intrinsic declarations (primary only — kv::table does not use secondary indices) +extern "C" { + __attribute__((sysio_wasm_import)) + int64_t kv_set(uint32_t key_format, uint64_t payer, const void* key, uint32_t key_size, const void* value, uint32_t value_size); + __attribute__((sysio_wasm_import)) + int32_t kv_get(uint32_t key_format, uint64_t code, const void* key, uint32_t key_size, void* value, uint32_t value_size); + __attribute__((sysio_wasm_import)) + int64_t kv_erase(uint32_t key_format, const void* key, uint32_t key_size); + __attribute__((sysio_wasm_import)) + int32_t kv_contains(uint32_t key_format, uint64_t code, const void* key, uint32_t key_size); + __attribute__((sysio_wasm_import)) + uint32_t kv_it_create(uint32_t key_format, uint64_t code, const void* prefix, uint32_t prefix_size); + __attribute__((sysio_wasm_import)) + void kv_it_destroy(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_it_status(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_it_next(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_it_prev(uint32_t handle); + __attribute__((sysio_wasm_import)) + int32_t kv_it_lower_bound(uint32_t handle, const void* key, uint32_t key_size); + __attribute__((sysio_wasm_import)) + int32_t kv_it_key(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size); + __attribute__((sysio_wasm_import)) + int32_t kv_it_value(uint32_t handle, uint32_t offset, void* dest, uint32_t dest_size, uint32_t* actual_size); +} + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifndef SYSIO_SAME_PAYER_DEFINED +#define SYSIO_SAME_PAYER_DEFINED +namespace sysio { + inline constexpr name same_payer{}; +} +#endif + +namespace sysio { namespace kv { + +namespace detail { + inline void encode_be64(char* buf, uint64_t v) { + for (int i = 7; i >= 0; --i) { buf[i] = static_cast(v & 0xFF); v >>= 8; } + } + inline uint64_t decode_be64(const char* buf) { + uint64_t v = 0; + for (int i = 0; i < 8; ++i) v = (v << 8) | static_cast(buf[i]); + return v; + } + + // Zero-copy for trivially_copyable types WITHOUT struct padding. + // Padding check: sizeof(T) == pack_size(T). If they differ, fall back to pack/unpack. + template + inline uint32_t store_value(const T& obj, char* buf, uint32_t buf_size) { + if constexpr (std::is_trivially_copyable::value) { + if (sizeof(T) == sysio::pack_size(obj)) { + memcpy(buf, &obj, sizeof(T)); + return sizeof(T); + } + } + sysio::datastream ds(buf, buf_size); + ds << obj; + return static_cast(ds.tellp()); + } + + template + inline void load_value(T& obj, const char* data, uint32_t size) { + if constexpr (std::is_trivially_copyable::value) { + if (size == sizeof(T)) { + memcpy(&obj, data, sizeof(T)); + return; + } + } + sysio::datastream ds(data, size); + ds >> obj; + } + + template + inline uint32_t value_size(const T& obj) { + if constexpr (std::is_trivially_copyable::value) { + if (sizeof(T) == sysio::pack_size(obj)) + return sizeof(T); + } + return sysio::pack_size(obj); + } +} + +template +class table { + static_assert(std::is_default_constructible::value, "Row type must be default constructible"); + + // Key layout: [table:8B BE][scope:8B BE][pk:8B BE] + static constexpr uint32_t scope_offset = 8; + static constexpr uint32_t pk_offset = 16; + static constexpr uint32_t key_size = 24; + static constexpr uint32_t prefix_size = 16; + static constexpr uint32_t tbl_prefix_size = 8; + static constexpr uint32_t stack_buf_size = 256; + + uint64_t _code; + uint64_t _scope; + + struct pk_buf { char data[key_size]; }; + struct prefix_buf { char data[prefix_size]; }; + struct table_prefix_buf { char data[tbl_prefix_size]; }; + + pk_buf make_pk(uint64_t pk) const { + pk_buf key; + detail::encode_be64(key.data, static_cast(TableName)); + detail::encode_be64(key.data + scope_offset, _scope); + detail::encode_be64(key.data + pk_offset, pk); + return key; + } + + prefix_buf _prefix; + prefix_buf make_prefix() const { + prefix_buf p; + detail::encode_be64(p.data, static_cast(TableName)); + detail::encode_be64(p.data + scope_offset, _scope); + return p; + } + + // Common kv_get + deserialize. Stack-allocates for values <= stack_buf_size. + bool get_value(const pk_buf& key, T& out) const { + char buf[stack_buf_size]; + int32_t sz = ::kv_get(kv_format_standard, _code, key.data, key_size, buf, stack_buf_size); + if (sz < 0) return false; + if (static_cast(sz) <= stack_buf_size) { + detail::load_value(out, buf, sz); + } else { + auto heap = std::make_unique(sz); + ::kv_get(kv_format_standard, _code, key.data, key_size, heap.get(), sz); + detail::load_value(out, heap.get(), sz); + } + return true; + } + + bool load_row(uint64_t pk, T& out) const { + return get_value(make_pk(pk), out); + } + + bool load_row_at(uint64_t scope, uint64_t pk, T& out) const { + pk_buf key; + detail::encode_be64(key.data, static_cast(TableName)); + detail::encode_be64(key.data + scope_offset, scope); + detail::encode_be64(key.data + pk_offset, pk); + return get_value(key, out); + } + + void store_row(uint64_t payer, uint64_t pk, const T& obj) const { + auto key = make_pk(pk); + uint32_t sz = detail::value_size(obj); + if (sz <= stack_buf_size) { + char buf[stack_buf_size]; + detail::store_value(obj, buf, stack_buf_size); + ::kv_set(kv_format_standard, payer, key.data, key_size, buf, sz); + } else { + auto heap = std::make_unique(sz); + detail::store_value(obj, heap.get(), sz); + ::kv_set(kv_format_standard, payer, key.data, key_size, heap.get(), sz); + } + } + +public: + table(sysio::name code, uint64_t scope) + : _code(code.value), _scope(scope), _prefix(make_prefix()) {} + + // --- const_iterator --- + struct const_iterator { + using iterator_category = std::bidirectional_iterator_tag; + using value_type = const T; + using difference_type = std::ptrdiff_t; + using pointer = const T*; + using reference = const T&; + + const_iterator() = default; + + const T& operator*() const { sysio::check(_has_obj, "deref invalid"); return _obj; } + const T* operator->() const { sysio::check(_has_obj, "deref invalid"); return &_obj; } + + const_iterator& operator++() { + if (!_has_obj) return *this; + ensure_handle(); + if (::kv_it_next(_handle) != 0) { _has_obj = false; } + else { load_from_handle(); } + return *this; + } + // Post-increment/decrement deleted: copy requires host function calls. Use ++it / --it. + const_iterator operator++(int) = delete; + const_iterator operator--(int) = delete; + + const_iterator& operator--() { + ensure_handle(); + if (::kv_it_prev(_handle) == 0) { _has_obj = true; load_from_handle(); } + else { _has_obj = false; } + return *this; + } + + friend bool operator==(const const_iterator& a, const const_iterator& b) { + if (!a._has_obj && !b._has_obj) return true; + if (!a._has_obj || !b._has_obj) return false; + return a._obj.primary_key() == b._obj.primary_key(); + } + friend bool operator!=(const const_iterator& a, const const_iterator& b) { return !(a == b); } + + ~const_iterator() { if (_handle >= 0) ::kv_it_destroy(_handle); } + const_iterator(const_iterator&& o) noexcept + : _tbl(o._tbl), _handle(o._handle), _has_obj(o._has_obj), _obj(std::move(o._obj)) + { o._handle = -1; o._has_obj = false; } + const_iterator& operator=(const_iterator&& o) noexcept { + if (this != &o) { + if (_handle >= 0) ::kv_it_destroy(_handle); + _tbl = o._tbl; _handle = o._handle; _has_obj = o._has_obj; _obj = std::move(o._obj); + o._handle = -1; o._has_obj = false; + } + return *this; + } + const_iterator(const const_iterator&) = delete; + const_iterator& operator=(const const_iterator&) = delete; + + private: + friend class table; + const table* _tbl = nullptr; + int32_t _handle = -1; + bool _has_obj = false; + T _obj; + + const_iterator(const table* t, const T& obj) : _tbl(t), _has_obj(true), _obj(obj) {} + const_iterator(const table* t, int32_t h, bool valid) : _tbl(t), _handle(h), _has_obj(valid) { + if (_has_obj) load_from_handle(); + } + static const_iterator make_end(const table* t) { const_iterator it; it._tbl = t; return it; } + + void load_from_handle() { + char key_buf[key_size]; uint32_t actual = 0; + if (::kv_it_key(_handle, 0, key_buf, key_size, &actual) != 0 || actual != key_size) { _has_obj = false; return; } + uint64_t pk = detail::decode_be64(key_buf + pk_offset); + _has_obj = _tbl->load_row(pk, _obj); + } + + void ensure_handle() { + if (_handle >= 0) return; + if (!_tbl) return; + _handle = ::kv_it_create(kv_format_standard, _tbl->_code, _tbl->_prefix.data, prefix_size); + if (_has_obj) { + auto key = _tbl->make_pk(_obj.primary_key()); + ::kv_it_lower_bound(_handle, key.data, key_size); + } + } + }; + + const_iterator end() const { return const_iterator::make_end(this); } + const_iterator cend() const { return end(); } + const_iterator begin() const { + uint32_t h = ::kv_it_create(kv_format_standard, _code, _prefix.data, prefix_size); + return const_iterator(this, h, ::kv_it_status(h) == 0); + } + const_iterator cbegin() const { return begin(); } + + // --- Cross-scope iteration (NEW: not possible with legacy multi_index) --- + // Uses 8-byte prefix (table only) to iterate ALL scopes for this table. + + struct scoped_row { + uint64_t scope; + uint64_t primary_key; + T obj; + }; + + struct scoped_iterator { + using iterator_category = std::forward_iterator_tag; + using value_type = const scoped_row; + using difference_type = std::ptrdiff_t; + using pointer = const scoped_row*; + using reference = const scoped_row&; + + scoped_iterator() = default; + + const scoped_row& operator*() const { sysio::check(_has_obj, "deref invalid"); return _row; } + const scoped_row* operator->() const { sysio::check(_has_obj, "deref invalid"); return &_row; } + + scoped_iterator& operator++() { + if (!_has_obj || _handle < 0) return *this; + if (::kv_it_next(_handle) != 0) { _has_obj = false; } + else { load_current(); } + return *this; + } + // Post-increment deleted: copy requires host function calls. Use ++it. + scoped_iterator operator++(int) = delete; + + friend bool operator==(const scoped_iterator& a, const scoped_iterator& b) { + if (!a._has_obj && !b._has_obj) return true; + if (!a._has_obj || !b._has_obj) return false; + return a._row.scope == b._row.scope && a._row.primary_key == b._row.primary_key; + } + friend bool operator!=(const scoped_iterator& a, const scoped_iterator& b) { return !(a == b); } + + ~scoped_iterator() { if (_handle >= 0) ::kv_it_destroy(_handle); } + scoped_iterator(scoped_iterator&& o) noexcept + : _tbl(o._tbl), _handle(o._handle), _has_obj(o._has_obj), _row(std::move(o._row)) + { o._handle = -1; o._has_obj = false; } + scoped_iterator& operator=(scoped_iterator&& o) noexcept { + if (this != &o) { + if (_handle >= 0) ::kv_it_destroy(_handle); + _tbl = o._tbl; _handle = o._handle; _has_obj = o._has_obj; _row = std::move(o._row); + o._handle = -1; o._has_obj = false; + } + return *this; + } + scoped_iterator(const scoped_iterator&) = delete; + scoped_iterator& operator=(const scoped_iterator&) = delete; + + private: + friend class table; + const table* _tbl = nullptr; + int32_t _handle = -1; + bool _has_obj = false; + scoped_row _row; + + scoped_iterator(const table* t, int32_t h, bool valid) : _tbl(t), _handle(h), _has_obj(valid) { + if (_has_obj) load_current(); + } + static scoped_iterator make_end(const table* t) { scoped_iterator it; it._tbl = t; return it; } + + void load_current() { + char key_buf[key_size]; uint32_t actual = 0; + if (::kv_it_key(_handle, 0, key_buf, key_size, &actual) != 0 || actual != key_size) { _has_obj = false; return; } + _row.scope = detail::decode_be64(key_buf + scope_offset); + _row.primary_key = detail::decode_be64(key_buf + pk_offset); + _has_obj = _tbl->load_row_at(_row.scope, _row.primary_key, _row.obj); + } + }; + + /// Iterate ALL rows across ALL scopes for this table. + /// Each entry provides scope, primary_key, and the deserialized row. + scoped_iterator begin_all_scopes() const { + table_prefix_buf tp; + detail::encode_be64(tp.data, static_cast(TableName)); + uint32_t h = ::kv_it_create(kv_format_standard, _code, tp.data, tbl_prefix_size); + return scoped_iterator(this, h, ::kv_it_status(h) == 0); + } + + scoped_iterator end_all_scopes() const { return scoped_iterator::make_end(this); } + + // Point lookup: 1 kv_get call, zero-copy for trivial types + const_iterator find(uint64_t pk) const { + T obj; + if (load_row(pk, obj)) return const_iterator(this, obj); + return end(); + } + + const_iterator require_find(uint64_t pk, const char* msg = "unable to find key") const { + auto itr = find(pk); sysio::check(itr != end(), msg); return itr; + } + + T get(uint64_t pk, const char* msg = "unable to find key") const { + T obj; + sysio::check(load_row(pk, obj), msg); + return obj; + } + + bool contains(uint64_t pk) const { + auto key = make_pk(pk); + return ::kv_contains(kv_format_standard, _code, key.data, key_size) != 0; + } + + const_iterator lower_bound(uint64_t pk) const { + auto key = make_pk(pk); + uint32_t h = ::kv_it_create(kv_format_standard, _code, _prefix.data, prefix_size); + return const_iterator(this, h, ::kv_it_lower_bound(h, key.data, key_size) == 0); + } + + const_iterator upper_bound(uint64_t pk) const { + if (pk == std::numeric_limits::max()) return end(); + return lower_bound(pk + 1); + } + + // --- Mutation --- + + template + const_iterator emplace(sysio::name payer, Lambda&& constructor) { + T obj; + constructor(obj); + store_row(payer.value, obj.primary_key(), obj); + return const_iterator(this, obj); + } + + // Set directly by primary key (convenience for raw structs) + void set(uint64_t pk, const T& obj) { + store_row(0, pk, obj); + } + + template + void modify(const const_iterator& itr, sysio::name payer, Lambda&& updater) { + sysio::check(itr._has_obj, "cannot modify end iterator"); + T& mobj = const_cast(itr._obj); + auto old_pk = mobj.primary_key(); + updater(mobj); + sysio::check(mobj.primary_key() == old_pk, "cannot modify primary key"); + store_row(payer.value, old_pk, mobj); + } + + template + void modify(const T& obj, sysio::name payer, Lambda&& updater) { + T mobj = obj; + auto old_pk = mobj.primary_key(); + updater(mobj); + sysio::check(mobj.primary_key() == old_pk, "cannot modify primary key"); + store_row(payer.value, old_pk, mobj); + } + + void erase(const T& obj) { + erase(obj.primary_key()); + } + + void erase(const const_iterator& itr) { + sysio::check(itr._has_obj, "cannot erase end iterator"); + erase(itr._obj.primary_key()); + } + + void erase(uint64_t pk) { + auto key = make_pk(pk); + ::kv_erase(kv_format_standard, key.data, key_size); + } + + uint64_t available_primary_key() const { + uint32_t h = ::kv_it_create(kv_format_standard, _code, _prefix.data, prefix_size); + if (::kv_it_status(h) != 0) { ::kv_it_destroy(h); return 0; } + char max_key[key_size]; + detail::encode_be64(max_key, static_cast(TableName)); + detail::encode_be64(max_key + scope_offset, _scope); + detail::encode_be64(max_key + pk_offset, std::numeric_limits::max()); + // lower_bound with UINT64_MAX pk positions at iterator_end (past all entries). + // Return value only indicates exact-match; iterator is validly positioned either way. + ::kv_it_lower_bound(h, max_key, key_size); + if (::kv_it_prev(h) != 0) { ::kv_it_destroy(h); return 0; } + char key_buf[key_size]; uint32_t actual = 0; + ::kv_it_key(h, 0, key_buf, key_size, &actual); + ::kv_it_destroy(h); + if (actual != key_size) return 0; + uint64_t last_pk = detail::decode_be64(key_buf + pk_offset); + sysio::check(last_pk < std::numeric_limits::max(), + "next primary key in table is at autoincrement limit"); + return last_pk + 1; + } +}; + +}} // namespace sysio::kv diff --git a/libraries/sysiolib/contracts/sysio/multi_index.hpp b/libraries/sysiolib/contracts/sysio/multi_index.hpp index 893eeaaa5..9c3c69ef6 100644 --- a/libraries/sysiolib/contracts/sysio/multi_index.hpp +++ b/libraries/sysiolib/contracts/sysio/multi_index.hpp @@ -4,1977 +4,14 @@ */ #pragma once -#include "../../contracts/sysio/action.hpp" -#include "../../core/sysio/name.hpp" -#include "../../core/sysio/serialize.hpp" -#include "../../core/sysio/fixed_bytes.hpp" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/** - * @defgroup multiindex Multi Index Table - * @ingroup contracts - */ +#include namespace sysio { - namespace internal_use_do_not_use { - extern "C" { - __attribute__((sysio_wasm_import)) - int32_t db_store_i64(uint64_t, uint64_t, uint64_t, uint64_t, const void*, uint32_t); - - __attribute__((sysio_wasm_import)) - void db_update_i64(int32_t, uint64_t, const void*, uint32_t); - - __attribute__((sysio_wasm_import)) - void db_remove_i64(int32_t); - - __attribute__((sysio_wasm_import)) - int32_t db_get_i64(int32_t, const void*, uint32_t); - - __attribute__((sysio_wasm_import)) - int32_t db_next_i64(int32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_previous_i64(int32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_find_i64(uint64_t, uint64_t, uint64_t, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_lowerbound_i64(uint64_t, uint64_t, uint64_t, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_upperbound_i64(uint64_t, uint64_t, uint64_t, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_end_i64(uint64_t, uint64_t, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx64_store(uint64_t, uint64_t, uint64_t, uint64_t, const uint64_t*); - - __attribute__((sysio_wasm_import)) - void db_idx64_update(int32_t, uint64_t, const uint64_t*); - - __attribute__((sysio_wasm_import)) - void db_idx64_remove(int32_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx64_next(int32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx64_previous(int32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx64_find_primary(uint64_t, uint64_t, uint64_t, uint64_t*, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx64_find_secondary(uint64_t, uint64_t, uint64_t, const uint64_t*, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx64_lowerbound(uint64_t, uint64_t, uint64_t, uint64_t*, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx64_upperbound(uint64_t, uint64_t, uint64_t, uint64_t*, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx64_end(uint64_t, uint64_t, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx128_store(uint64_t, uint64_t, uint64_t, uint64_t, const uint128_t*); - - __attribute__((sysio_wasm_import)) - void db_idx128_update(int32_t, uint64_t, const uint128_t*); - - __attribute__((sysio_wasm_import)) - void db_idx128_remove(int32_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx128_next(int32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx128_previous(int32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx128_find_primary(uint64_t, uint64_t, uint64_t, uint128_t*, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx128_find_secondary(uint64_t, uint64_t, uint64_t, const uint128_t*, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx128_lowerbound(uint64_t, uint64_t, uint64_t, uint128_t*, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx128_upperbound(uint64_t, uint64_t, uint64_t, uint128_t*, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx128_end(uint64_t, uint64_t, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx256_store(uint64_t, uint64_t, uint64_t, uint64_t, const uint128_t*, uint32_t); - - __attribute__((sysio_wasm_import)) - void db_idx256_update(int32_t, uint64_t, const uint128_t*, uint32_t); - - __attribute__((sysio_wasm_import)) - void db_idx256_remove(int32_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx256_next(int32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx256_previous(int32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx256_find_primary(uint64_t, uint64_t, uint64_t, uint128_t*, uint32_t, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx256_find_secondary(uint64_t, uint64_t, uint64_t, const uint128_t*, uint32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx256_lowerbound(uint64_t, uint64_t, uint64_t, uint128_t*, uint32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx256_upperbound(uint64_t, uint64_t, uint64_t, uint128_t*, uint32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx256_end(uint64_t, uint64_t, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_double_store(uint64_t, uint64_t, uint64_t, uint64_t, const double*); - - __attribute__((sysio_wasm_import)) - void db_idx_double_update(int32_t, uint64_t, const double*); - - __attribute__((sysio_wasm_import)) - void db_idx_double_remove(int32_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_double_next(int32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_double_previous(int32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_double_find_primary(uint64_t, uint64_t, uint64_t, double*, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_double_find_secondary(uint64_t, uint64_t, uint64_t, const double*, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_double_lowerbound(uint64_t, uint64_t, uint64_t, double*, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_double_upperbound(uint64_t, uint64_t, uint64_t, double*, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_double_end(uint64_t, uint64_t, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_long_double_store(uint64_t, uint64_t, uint64_t, uint64_t, const long double*); - - __attribute__((sysio_wasm_import)) - void db_idx_long_double_update(int32_t, uint64_t, const long double*); - - __attribute__((sysio_wasm_import)) - void db_idx_long_double_remove(int32_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_long_double_next(int32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_long_double_previous(int32_t, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_long_double_find_primary(uint64_t, uint64_t, uint64_t, long double*, uint64_t); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_long_double_find_secondary(uint64_t, uint64_t, uint64_t, const long double*, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_long_double_lowerbound(uint64_t, uint64_t, uint64_t, long double*, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_long_double_upperbound(uint64_t, uint64_t, uint64_t, long double*, uint64_t*); - - __attribute__((sysio_wasm_import)) - int32_t db_idx_long_double_end(uint64_t, uint64_t, uint64_t); - } - }; - -constexpr static inline name same_payer{}; - -template -struct const_mem_fun -{ - typedef typename std::remove_reference::type result_type; - - template - - auto operator()(const ChainedPtr& x)const -> std::enable_if_t::value, Type> - { - return operator()(*x); - } - - Type operator()(const Class& x)const - { - return (x.*PtrToMemberFunction)(); - } - - Type operator()(const std::reference_wrapper& x)const - { - return operator()(x.get()); - } - - Type operator()(const std::reference_wrapper& x)const - { - return operator()(x.get()); - } -}; - -#define WRAP_SECONDARY_SIMPLE_TYPE(IDX, TYPE)\ -template<>\ -struct secondary_index_db_functions {\ - static int32_t db_idx_next( int32_t iterator, uint64_t* primary ) { return internal_use_do_not_use::db_##IDX##_next( iterator, primary ); } \ - static int32_t db_idx_previous( int32_t iterator, uint64_t* primary ) { return internal_use_do_not_use::db_##IDX##_previous( iterator, primary ); } \ - static void db_idx_remove( int32_t iterator ) { internal_use_do_not_use::db_##IDX##_remove( iterator ); } \ - static int32_t db_idx_end( uint64_t code, uint64_t scope, uint64_t table ) { return internal_use_do_not_use::db_##IDX##_end( code, scope, table ); } \ - static int32_t db_idx_store( uint64_t scope, uint64_t table, uint64_t payer, uint64_t id, const TYPE& secondary ) {\ - return internal_use_do_not_use::db_##IDX##_store( scope, table, payer, id, &secondary ); \ - }\ - static void db_idx_update( int32_t iterator, uint64_t payer, const TYPE& secondary ) {\ - internal_use_do_not_use::db_##IDX##_update( iterator, payer, &secondary ); \ - }\ - static int32_t db_idx_find_primary( uint64_t code, uint64_t scope, uint64_t table, uint64_t primary, TYPE& secondary ) {\ - return internal_use_do_not_use::db_##IDX##_find_primary( code, scope, table, &secondary, primary ); \ - }\ - static int32_t db_idx_find_secondary( uint64_t code, uint64_t scope, uint64_t table, const TYPE& secondary, uint64_t& primary ) {\ - return internal_use_do_not_use::db_##IDX##_find_secondary( code, scope, table, &secondary, &primary ); \ - }\ - static int32_t db_idx_lowerbound( uint64_t code, uint64_t scope, uint64_t table, TYPE& secondary, uint64_t& primary ) {\ - return internal_use_do_not_use::db_##IDX##_lowerbound( code, scope, table, &secondary, &primary ); \ - }\ - static int32_t db_idx_upperbound( uint64_t code, uint64_t scope, uint64_t table, TYPE& secondary, uint64_t& primary ) {\ - return internal_use_do_not_use::db_##IDX##_upperbound( code, scope, table, &secondary, &primary ); \ - }\ -}; - -#define WRAP_SECONDARY_ARRAY_TYPE(IDX, TYPE)\ -template<>\ -struct secondary_index_db_functions {\ - static int32_t db_idx_next( int32_t iterator, uint64_t* primary ) { return internal_use_do_not_use::db_##IDX##_next( iterator, primary ); } \ - static int32_t db_idx_previous( int32_t iterator, uint64_t* primary ) { return internal_use_do_not_use::db_##IDX##_previous( iterator, primary ); } \ - static void db_idx_remove( int32_t iterator ) { internal_use_do_not_use::db_##IDX##_remove( iterator ); } \ - static int32_t db_idx_end( uint64_t code, uint64_t scope, uint64_t table ) { return internal_use_do_not_use::db_##IDX##_end( code, scope, table ); } \ - static int32_t db_idx_store( uint64_t scope, uint64_t table, uint64_t payer, uint64_t id, const TYPE& secondary ) {\ - return internal_use_do_not_use::db_##IDX##_store( scope, table, payer, id, secondary.data(), TYPE::num_words() ); \ - }\ - static void db_idx_update( int32_t iterator, uint64_t payer, const TYPE& secondary ) {\ - internal_use_do_not_use::db_##IDX##_update( iterator, payer, secondary.data(), TYPE::num_words() ); \ - }\ - static int32_t db_idx_find_primary( uint64_t code, uint64_t scope, uint64_t table, uint64_t primary, TYPE& secondary ) {\ - return internal_use_do_not_use::db_##IDX##_find_primary( code, scope, table, secondary.data(), TYPE::num_words(), primary ); \ - }\ - static int32_t db_idx_find_secondary( uint64_t code, uint64_t scope, uint64_t table, const TYPE& secondary, uint64_t& primary ) {\ - return internal_use_do_not_use::db_##IDX##_find_secondary( code, scope, table, secondary.data(), TYPE::num_words(), &primary ); \ - }\ - static int32_t db_idx_lowerbound( uint64_t code, uint64_t scope, uint64_t table, TYPE& secondary, uint64_t& primary ) {\ - return internal_use_do_not_use::db_##IDX##_lowerbound( code, scope, table, secondary.data(), TYPE::num_words(), &primary ); \ - }\ - static int32_t db_idx_upperbound( uint64_t code, uint64_t scope, uint64_t table, TYPE& secondary, uint64_t& primary ) {\ - return internal_use_do_not_use::db_##IDX##_upperbound( code, scope, table, secondary.data(), TYPE::num_words(), &primary ); \ - }\ -}; - -#define MAKE_TRAITS_FOR_ARITHMETIC_SECONDARY_KEY(TYPE)\ -template<>\ -struct secondary_key_traits {\ - static_assert( std::numeric_limits::is_specialized, "TYPE does not have specialized numeric_limits" );\ - static constexpr TYPE true_lowest() { return std::numeric_limits::lowest(); }\ -}; - -namespace _multi_index_detail { - - template - struct secondary_index_db_functions; - - template - struct secondary_key_traits; - - WRAP_SECONDARY_SIMPLE_TYPE(idx64, uint64_t) - MAKE_TRAITS_FOR_ARITHMETIC_SECONDARY_KEY(uint64_t) - - WRAP_SECONDARY_SIMPLE_TYPE(idx128, uint128_t) - MAKE_TRAITS_FOR_ARITHMETIC_SECONDARY_KEY(uint128_t) - - WRAP_SECONDARY_SIMPLE_TYPE(idx_double, double) - template<> - struct secondary_key_traits { - static constexpr double true_lowest() { return -std::numeric_limits::infinity(); } - }; - - WRAP_SECONDARY_SIMPLE_TYPE(idx_long_double, long double) - template<> - struct secondary_key_traits { - static constexpr long double true_lowest() { return -std::numeric_limits::infinity(); } - }; - - WRAP_SECONDARY_ARRAY_TYPE(idx256, sysio::fixed_bytes<32>) - template<> - struct secondary_key_traits> { - static constexpr sysio::fixed_bytes<32> true_lowest() { return sysio::fixed_bytes<32>(); } - }; - - template - inline uint64_t to_raw_key(PK pk) { return pk; } - inline uint64_t to_raw_key(sysio::name pk) { return pk.value; } - -} - -/** - * The indexed_by struct is used to instantiate the indices for the Multi-Index table. In SYSIO, up to 16 secondary indices can be specified. - * - * @ingroup multiindex - * @tparam IndexName - is the name of the index. The name must be provided as an SYSIO base32 encoded 64-bit integer and must conform to the SYSIO naming requirements of a maximum of 13 characters, the first twelve from the lowercase characters a-z, digits 1-5, and ".", and if there is a 13th character, it is restricted to lowercase characters a-p and ".". - * @tparam Extractor - is a function call operator that takes a const reference to the table object type and returns either a secondary key type or a reference to a secondary key type. It is recommended to use the `sysio::const_mem_fun` template. - * - * Example: - * - * - * @code - * #include - * using namespace sysio; - * class mycontract: sysio::contract { - * struct record { - * uint64_t primary; - * uint128_t secondary; - * uint64_t primary_key() const { return primary; } - * uint128_t get_secondary() const { return secondary; } - * }; - * public: - * mycontract(name receiver, name code, datastream ds):contract(receiver, code, ds){} - * void myaction() { - * auto code = _self; - * auto scope = _self; - * multi_index<"mytable"_n, record, - * indexed_by< "bysecondary"_n, const_mem_fun > > table( code, scope); - * } - * } - * SYSIO_DISPATCH( mycontract, (myaction) ) - * @endcode - */ -template -struct indexed_by { - enum constants { index_name = static_cast(IndexName) }; - typedef Extractor secondary_extractor_type; -}; - -/** - * @ingroup multiindex - * - * @brief Defines SYSIO Multi Index Table - * @details SYSIO Multi-Index API provides a C++ interface to the SYSIO database. It is patterned after Boost Multi Index Container. - * SYSIO Multi-Index table requires exactly a uint64_t primary key. For the table to be able to retrieve the primary key, - * the object stored inside the table is required to have a const member function called primary_key() that returns uint64_t. - * SYSIO Multi-Index table also supports up to 16 secondary indices. The type of the secondary indices could be any of: - * - uint64_t - * - uint128_t - * - double - * - long double - * - sysio::checksum256 - * - * @tparam TableName - name of the table - * @tparam T - type of the data stored inside the table - * @tparam Indices - secondary indices for the table, up to 16 indices is supported here - * - * Example: - * - * @code - * #include - * using namespace sysio; - * class mycontract: contract { - * struct record { - * uint64_t primary; - * uint64_t secondary_1; - * uint128_t secondary_2; - * checksum256 secondary_3; - * double secondary_4; - * long double secondary_5; - * uint64_t primary_key() const { return primary; } - * uint64_t get_secondary_1() const { return secondary_1; } - * uint128_t get_secondary_2() const { return secondary_2; } - * checksum256 get_secondary_3() const { return secondary_3; } - * double get_secondary_4() const { return secondary_4; } - * long double get_secondary_5() const { return secondary_5; } - * }; - * public: - * mycontract(name receiver, name code, datastream ds):contract(receiver, code, ds){} - * void myaction() { - * auto code = _self; - * auto scope = _self; - * multi_index<"mytable"_n, record, - * indexed_by< "bysecondary1"_n, const_mem_fun >, - * indexed_by< "bysecondary2"_n, const_mem_fun >, - * indexed_by< "bysecondary3"_n, const_mem_fun >, - * indexed_by< "bysecondary4"_n, const_mem_fun >, - * indexed_by< "bysecondary5"_n, const_mem_fun > - * > table( code, scope); - * } - * } - * SYSIO_DISPATCH( mycontract, (myaction) ) - * @endcode - */ - -template -class multi_index -{ - private: - - static_assert( std::is_same_v().primary_key())), uint64_t>, - "Primary key must be uint64_t or name" ); - static_assert( sizeof...(Indices) <= 16, "multi_index only supports a maximum of 16 secondary indices" ); - - constexpr static bool validate_table_name( name n ) { - // Limit table names to 12 characters so that the last character (4 bits) can be used to distinguish between the secondary indices. - return n.length() < 13; //(n & 0x000000000000000FULL) == 0; - } - - constexpr static size_t max_stack_buffer_size = 512; - - static_assert( validate_table_name( name(TableName) ), "multi_index does not support table names with a length greater than 12"); - - name _code; - uint64_t _scope; - - mutable uint64_t _next_primary_key; - - enum next_primary_key_tags : uint64_t { - no_available_primary_key = static_cast(-2), // Must be the smallest uint64_t value compared to all other tags - unset_next_primary_key = static_cast(-1) - }; - - struct item : public T - { - template - item( const multi_index* idx, Constructor&& c ) - :__idx(idx){ - c(*this); - } - - const multi_index* __idx; - int32_t __primary_itr; - int32_t __iters[sizeof...(Indices)+(sizeof...(Indices)==0)]; - }; - - struct item_ptr - { - item_ptr(std::unique_ptr&& i, uint64_t pk, int32_t pitr) - : _item(std::move(i)), _primary_key(pk), _primary_itr(pitr) {} - - std::unique_ptr _item; - uint64_t _primary_key; - int32_t _primary_itr; - }; - - mutable std::vector _items_vector; - - template - struct index { - public: - typedef Extractor secondary_extractor_type; - typedef typename std::decay::type secondary_key_type; - - constexpr static bool validate_index_name( sysio::name n ) { - return n.value != 0 && n != sysio::name("primary"); // Primary is a reserve index name. - } - - static_assert( validate_index_name( name(IndexName) ), "invalid index name used in multi_index" ); - - enum constants { - table_name = static_cast(TableName), - index_name = static_cast(IndexName), - index_number = Number, - index_table_name = (static_cast(TableName) & 0xFFFFFFFFFFFFFFF0ULL) - | (Number & 0x000000000000000FULL) // Assuming no more than 16 secondary indices are allowed - }; - - constexpr static uint64_t name() { return index_table_name; } - constexpr static uint64_t number() { return Number; } - - struct const_iterator { - public: - using iterator_category = std::bidirectional_iterator_tag; - using value_type = const T; - using difference_type = std::ptrdiff_t; - using pointer = const T*; - using reference = const T&; - friend bool operator == ( const const_iterator& a, const const_iterator& b ) { - return a._item == b._item; - } - friend bool operator != ( const const_iterator& a, const const_iterator& b ) { - return a._item != b._item; - } - - const T& operator*()const { return *static_cast(_item); } - const T* operator->()const { return static_cast(_item); } - - const_iterator operator++(int){ - const_iterator result(*this); - ++(*this); - return result; - } - - const_iterator operator--(int){ - const_iterator result(*this); - --(*this); - return result; - } - - const_iterator& operator++() { - using namespace _multi_index_detail; - - sysio::check( _item != nullptr, "cannot increment end iterator" ); - - if( _item->__iters[Number] == -1 ) { - secondary_key_type temp_secondary_key; - auto idxitr = secondary_index_db_functions::db_idx_find_primary(_idx->get_code().value, _idx->get_scope(), _idx->name(), _item->primary_key(), temp_secondary_key); - auto& mi = const_cast( *_item ); - mi.__iters[Number] = idxitr; - } - - uint64_t next_pk = 0; - auto next_itr = secondary_index_db_functions::db_idx_next( _item->__iters[Number], &next_pk ); - if( next_itr < 0 ) { - _item = nullptr; - return *this; - } - - const T& obj = *_idx->_multidx->find( next_pk ); - auto& mi = const_cast( static_cast(obj) ); - mi.__iters[Number] = next_itr; - _item = &mi; - - return *this; - } - - const_iterator& operator--() { - using namespace _multi_index_detail; - - uint64_t prev_pk = 0; - int32_t prev_itr = -1; - - if( !_item ) { - auto ei = secondary_index_db_functions::db_idx_end(_idx->get_code().value, _idx->get_scope(), _idx->name()); - sysio::check( ei != -1, "cannot decrement end iterator when the index is empty" ); - prev_itr = secondary_index_db_functions::db_idx_previous( ei , &prev_pk ); - sysio::check( prev_itr >= 0, "cannot decrement end iterator when the index is empty" ); - } else { - if( _item->__iters[Number] == -1 ) { - secondary_key_type temp_secondary_key; - auto idxitr = secondary_index_db_functions::db_idx_find_primary(_idx->get_code().value, _idx->get_scope(), _idx->name(), _item->primary_key(), temp_secondary_key); - auto& mi = const_cast( *_item ); - mi.__iters[Number] = idxitr; - } - prev_itr = secondary_index_db_functions::db_idx_previous( _item->__iters[Number], &prev_pk ); - sysio::check( prev_itr >= 0, "cannot decrement iterator at beginning of index" ); - } - - const T& obj = *_idx->_multidx->find( prev_pk ); - auto& mi = const_cast( static_cast(obj) ); - mi.__iters[Number] = prev_itr; - _item = &mi; - - return *this; - } - - const_iterator():_item(nullptr){} - private: - friend struct index; - const_iterator( const index* idx, const item* i = nullptr ) - : _idx(idx), _item(i) {} - - const index* _idx; - const item* _item; - }; /// struct multi_index::index::const_iterator - - typedef std::reverse_iterator const_reverse_iterator; - - const_iterator cbegin()const { - using namespace _multi_index_detail; - return lower_bound( secondary_key_traits::true_lowest() ); - } - const_iterator begin()const { return cbegin(); } - - const_iterator cend()const { return const_iterator( this ); } - const_iterator end()const { return cend(); } - - const_reverse_iterator crbegin()const { return std::make_reverse_iterator(cend()); } - const_reverse_iterator rbegin()const { return crbegin(); } - - const_reverse_iterator crend()const { return std::make_reverse_iterator(cbegin()); } - const_reverse_iterator rend()const { return crend(); } - - const_iterator find( secondary_key_type&& secondary )const { - return find( secondary ); - } - - const_iterator find( const secondary_key_type& secondary )const { - auto lb = lower_bound( secondary ); - auto e = cend(); - if( lb == e ) return e; - - if( secondary != secondary_extractor_type()(*lb) ) - return e; - return lb; - } - - const_iterator require_find( secondary_key_type&& secondary, const char* error_msg = "unable to find secondary key" )const { - return require_find( secondary, error_msg ); - } - - const_iterator require_find( const secondary_key_type& secondary, const char* error_msg = "unable to find secondary key" )const { - auto lb = lower_bound( secondary ); - sysio::check( lb != cend(), error_msg ); - sysio::check( secondary == secondary_extractor_type()(*lb), error_msg ); - return lb; - } - - /** - * Gets the object with the smallest primary key in the case where the secondary key is not unique. - * - * Avoid the common pitfall of copy-assigning the T& reference returned - * to a stack-allocated local variable and then passing that into modify of the multi-index. - * The most common mistake is when the local variable is defined as auto - * typename, instead it should be of type auto& or decltype(auto). - */ - const T& get( secondary_key_type&& secondary, const char* error_msg = "unable to find secondary key" )const { - return get( secondary, error_msg ); - } - - /** - * Gets the object with the smallest primary key in the case where the secondary key is not unique. - * - * Avoid the common pitfall of copy-assigning the T& reference returned - * to a stack-allocated local variable and then passing that into modify of the multi-index. - * The most common mistake is when the local variable is defined as auto - * typename, instead it should be of type auto& or decltype(auto). - */ - const T& get( const secondary_key_type& secondary, const char* error_msg = "unable to find secondary key" )const { - auto result = find( secondary ); - sysio::check( result != cend(), error_msg ); - return *result; - } - - const_iterator lower_bound( secondary_key_type&& secondary )const { - return lower_bound( secondary ); - } - const_iterator lower_bound( const secondary_key_type& secondary )const { - using namespace _multi_index_detail; - - uint64_t primary = 0; - secondary_key_type secondary_copy(secondary); - auto itr = secondary_index_db_functions::db_idx_lowerbound( get_code().value, get_scope(), name(), secondary_copy, primary ); - if( itr < 0 ) return cend(); - - const T& obj = *_multidx->find( primary ); - auto& mi = const_cast( static_cast(obj) ); - mi.__iters[Number] = itr; - - return {this, &mi}; - } - - const_iterator upper_bound( secondary_key_type&& secondary )const { - return upper_bound( secondary ); - } - const_iterator upper_bound( const secondary_key_type& secondary )const { - using namespace _multi_index_detail; - - uint64_t primary = 0; - secondary_key_type secondary_copy(secondary); - auto itr = secondary_index_db_functions::db_idx_upperbound( get_code().value, get_scope(), name(), secondary_copy, primary ); - if( itr < 0 ) return cend(); - - const T& obj = *_multidx->find( primary ); - auto& mi = const_cast( static_cast(obj) ); - mi.__iters[Number] = itr; - - return {this, &mi}; - } - /** - * Warning: the interator_to can have undefined behavior if the caller - * passes in a reference to a stack-allocated object rather than the - * reference returned by get or by dereferencing a const_iterator. - */ - const_iterator iterator_to( const T& obj ) { - using namespace _multi_index_detail; - - const auto& objitem = static_cast(obj); - sysio::check( objitem.__idx == _multidx, "object passed to iterator_to is not in multi_index" ); - - if( objitem.__iters[Number] == -1 ) { - secondary_key_type temp_secondary_key; - auto idxitr = secondary_index_db_functions::db_idx_find_primary(get_code().value, get_scope(), name(), objitem.primary_key(), temp_secondary_key); - auto& mi = const_cast( objitem ); - mi.__iters[Number] = idxitr; - } - - return {this, &objitem}; - } - - template - void modify( const_iterator itr, sysio::name payer, Lambda&& updater ) { - sysio::check( itr != cend(), "cannot pass end iterator to modify" ); - - _multidx->modify( *itr, payer, std::forward(updater) ); - } - - template - void modify( const T& obj, sysio::name payer, Lambda&& updater ) { - _multidx->modify( obj, payer, std::forward(updater) ); - } - - const_iterator erase( const_iterator itr ) { - sysio::check( itr != cend(), "cannot pass end iterator to erase" ); - - const auto& obj = *itr; - ++itr; - - _multidx->erase(obj); - - return itr; - } - - sysio::name get_code()const { return _multidx->get_code(); } - uint64_t get_scope()const { return _multidx->get_scope(); } - - 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; - - index( typename std::conditional::type midx ) - :_multidx(midx){} - - typename std::conditional::type _multidx; - }; /// struct multi_index::index - - template - struct intc { enum e{ value = I }; operator uint64_t()const{ return I; } }; - enum index_cv { const_index = 0, mutable_index = 1 }; - - template - class make_index_tuple { - - 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, const_index>{}, - index(Values::index_name)), - typename Values::secondary_extractor_type, - intc::e::value, mutable_index>{})...); - } - public: - using type = decltype( get_type(std::make_index_sequence{}) ); - }; - - using indices_type = typename make_index_tuple::type; - - class make_extractor_tuple { - 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 constexpr auto get_extractor_tuple(IndicesType, const Obj& obj) { - return extractor_tuple(IndicesType{}, obj, std::make_index_sequence>{}); - } - }; - - const item& load_object_by_primary_iterator( int32_t itr )const { - using namespace _multi_index_detail; - - auto itr2 = std::find_if(_items_vector.rbegin(), _items_vector.rend(), [&](const item_ptr& ptr) { - return ptr._primary_itr == itr; - }); - if( itr2 != _items_vector.rend() ) - return *itr2->_item; - - auto size = internal_use_do_not_use::db_get_i64( itr, nullptr, 0 ); - sysio::check( size >= 0, "error reading iterator" ); - - //using malloc/free here potentially is not exception-safe, although WASM doesn't support exceptions - void* buffer = max_stack_buffer_size < size_t(size) ? malloc(size_t(size)) : alloca(size_t(size)); - - internal_use_do_not_use::db_get_i64( itr, buffer, uint32_t(size) ); - - datastream ds( (char*)buffer, uint32_t(size) ); - - auto itm = std::make_unique( this, [&]( auto& i ) { - T& val = static_cast(i); - ds >> val; - - i.__primary_itr = itr; - bluegrass::meta::for_each(indices_type{}, [&](auto idx){ - typedef std::tuple_element_t index_type; - i.__iters[ index_type::number() ] = -1; - }); - }); - - const item* ptr = itm.get(); - auto pk = _multi_index_detail::to_raw_key(itm->primary_key()); - auto pitr = itm->__primary_itr; - - _items_vector.emplace_back( std::move(itm), pk, pitr ); - - if ( max_stack_buffer_size < size_t(size) ) { - free(buffer); - } - - return *ptr; - } /// load_object_by_primary_iterator - - public: - /** - * Constructs an instance of a Multi-Index table. - * @ingroup multiindex - * - * @param code - Account that owns table - * @param scope - Scope identifier within the code hierarchy - * - * @pre code and scope member properties are initialized - * @post each secondary index table initialized - * @post Secondary indices are updated to refer to the newly added object. If the secondary index tables do not exist, they are created. - * @post The payer is charged for the storage usage of the new object and, if the table (and secondary index tables) must be created, for the overhead of the table creation. - * - * Notes - * The `sysio::multi_index` template has template parameters ``, where: - * - `TableName` is the name of the table, maximum 12 characters long, characters in the name from the set of lowercase letters, digits 1 to 5, and the "." (period) character and is converted to a sysio::raw - which wraps uint64_t; - * - `T` is the object type (i.e., row definition); - * - `Indices` is a list of up to 16 secondary indices. - * - Each must be a default constructable class or struct - * - Each must have a function call operator that takes a const reference to the table object type and returns either a secondary key type or a reference to a secondary key type - * - It is recommended to use the sysio::const_mem_fun template - * - * Example: - * - * @code - * #include - * using namespace sysio; - * using namespace std; - * class addressbook: contract { - * struct address { - * uint64_t account_name; - * string first_name; - * string last_name; - * string street; - * string city; - * string state; - * uint64_t primary_key() const { return account_name; } - * }; - * public: - * addressbook(name self):contract(self) {} - * typedef sysio::multi_index< "address"_n, address > address_index; - * void myaction() { - * address_index addresses(_self, _self.value); // code, scope - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - multi_index( name code, uint64_t scope ) - :_code(code),_scope(scope),_next_primary_key(unset_next_primary_key) - {} - - /** - * Returns the `code` member property. - * @ingroup multiindex - * - * @return Account name of the Code that owns the Primary Table. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * address_index addresses("dan"_n, "dan"_n.value); // code, scope - * sysio::check(addresses.get_code() == "dan"_n, "Codes don't match."); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - name get_code()const { return _code; } - - /** - * Returns the `scope` member property. - * @ingroup multiindex - * - * @return Scope id of the Scope within the Code of the Current Receiver under which the desired Primary Table instance can be found. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * address_index addresses("dan"_n, "dan"_n.value); // code, scope - * sysio::check(addresses.get_scope() == "dan"_n.value, "Scopes don't match"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - uint64_t get_scope()const { return _scope; } - - struct const_iterator { - using iterator_category = std::bidirectional_iterator_tag; - using value_type = const T; - using difference_type = std::ptrdiff_t; - using pointer = const T*; - using reference = const T&; - - friend bool operator == ( const const_iterator& a, const const_iterator& b ) { - return a._item == b._item; - } - friend bool operator != ( const const_iterator& a, const const_iterator& b ) { - return a._item != b._item; - } - - const T& operator*()const { return *static_cast(_item); } - const T* operator->()const { return static_cast(_item); } - - const_iterator operator++(int) { - const_iterator result(*this); - ++(*this); - return result; - } - - const_iterator operator--(int) { - const_iterator result(*this); - --(*this); - return result; - } - - const_iterator& operator++() { - sysio::check( _item != nullptr, "cannot increment end iterator" ); - - uint64_t next_pk; - auto next_itr = internal_use_do_not_use::db_next_i64( _item->__primary_itr, &next_pk ); - if( next_itr < 0 ) - _item = nullptr; - else - _item = &_multidx->load_object_by_primary_iterator( next_itr ); - return *this; - } - const_iterator& operator--() { - uint64_t prev_pk; - int32_t prev_itr = -1; - - if( !_item ) { - auto ei = internal_use_do_not_use::db_end_i64(_multidx->get_code().value, _multidx->get_scope(), static_cast(TableName)); - sysio::check( ei != -1, "cannot decrement end iterator when the table is empty" ); - prev_itr = internal_use_do_not_use::db_previous_i64( ei , &prev_pk ); - sysio::check( prev_itr >= 0, "cannot decrement end iterator when the table is empty" ); - } else { - prev_itr = internal_use_do_not_use::db_previous_i64( _item->__primary_itr, &prev_pk ); - sysio::check( prev_itr >= 0, "cannot decrement iterator at beginning of table" ); - } - - _item = &_multidx->load_object_by_primary_iterator( prev_itr ); - return *this; - } - - private: - const_iterator( const multi_index* mi, const item* i = nullptr ) - :_multidx(mi),_item(i){} - - const multi_index* _multidx; - const item* _item; - friend class multi_index; - }; /// struct multi_index::const_iterator - - typedef std::reverse_iterator const_reverse_iterator; - - /** - * Returns an iterator pointing to the object_type with the lowest primary key value in the Multi-Index table. - * @ingroup multiindex - * - * @return An iterator pointing to the object_type with the lowest primary key value in the Multi-Index table. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * - * auto itr = addresses.find("dan"_n); - * sysio::check(itr == addresses.cbegin(), "Only address is not at front."); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - const_iterator cbegin()const { - return lower_bound(std::numeric_limits::lowest()); - } - - /** - * Returns an iterator pointing to the object_type with the lowest primary key value in the Multi-Index table. - * @ingroup multiindex - * - * @return An iterator pointing to the object_type with the lowest primary key value in the Multi-Index table. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * - * auto itr = addresses.find("dan"_n); - * sysio::check(itr == addresses.begin(), "Only address is not at front."); - * } - * } - * SYSIO_ABI( addressbook, (myaction) ) - * @endcode - */ - const_iterator begin()const { return cbegin(); } - - /** - * Returns an iterator referring to the `past-the-end` element in the multi index container. The `past-the-end` element is the theoretical element that would follow the last element in the vector. It does not point to any element, and thus shall not be dereferenced. - * @ingroup multiindex - * - * @return An iterator referring to the `past-the-end` element in the multi index container. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * - * auto itr = addresses.find("dan"_n); - * sysio::check(itr != addresses.cend(), "Address for account doesn't exist"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - const_iterator cend()const { return const_iterator( this ); } - - /** - * Returns an iterator referring to the `past-the-end` element in the multi index container. The `past-the-end` element is the theoretical element that would follow the last element in the vector. It does not point to any element, and thus shall not be dereferenced. - * @ingroup multiindex - * - * @return An iterator referring to the `past-the-end` element in the multi index container. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * - * auto itr = addresses.find("dan"_n); - * sysio::check(itr != addresses.end(), "Address for account doesn't exist"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - const_iterator end()const { return cend(); } - - /** - * Returns a reverse iterator pointing to the `object_type` with the highest primary key value in the Multi-Index table. - * @ingroup multiindex - * - * @return A reverse iterator pointing to the `object_type` with the highest primary key value in the Multi-Index table. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * // add additional account - brendan - * - * addresses.emplace(payer, [&](auto& address) { - * address.account_name = "brendan"_n; - * address.first_name = "Brendan"; - * address.last_name = "Blumer"; - * address.street = "1 SYS Way"; - * address.city = "Hong Kong"; - * address.state = "HK"; - * }); - * auto itr = addresses.crbegin(); - * sysio::check(itr->account_name == name("dan"), "Lock arf, Incorrect Last Record "); - * itr++; - * sysio::check(itr->account_name == name("brendan"), "Lock arf, Incorrect Second Last Record"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - const_reverse_iterator crbegin()const { return std::make_reverse_iterator(cend()); } - - /** - * Returns a reverse iterator pointing to the `object_type` with the highest primary key value in the Multi-Index table. - * @ingroup multiindex - * - * @return A reverse iterator pointing to the `object_type` with the highest primary key value in the Multi-Index table. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * // add additional account - brendan - * - * addresses.emplace(payer, [&](auto& address) { - * address.account_name = "brendan"_n; - * address.first_name = "Brendan"; - * address.last_name = "Blumer"; - * address.street = "1 SYS Way"; - * address.city = "Hong Kong"; - * address.state = "HK"; - * }); - * auto itr = addresses.rbegin(); - * sysio::check(itr->account_name == name("dan"), "Lock arf, Incorrect Last Record "); - * itr++; - * sysio::check(itr->account_name == name("brendan"), "Lock arf, Incorrect Second Last Record"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - const_reverse_iterator rbegin()const { return crbegin(); } - - /** - * Returns an iterator pointing to the `object_type` with the lowest primary key value in the Multi-Index table. - * @ingroup multiindex - * - * @return An iterator pointing to the `object_type` with the lowest primary key value in the Multi-Index table. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * // add additional account - brendan - * - * addresses.emplace(payer, [&](auto& address) { - * address.account_name = "brendan"_n; - * address.first_name = "Brendan"; - * address.last_name = "Blumer"; - * address.street = "1 SYS Way"; - * address.city = "Hong Kong"; - * address.state = "HK"; - * }); - * auto itr = addresses.crend(); - * itr--; - * sysio::check(itr->account_name == name("brendan"), "Lock arf, Incorrect First Record "); - * itr--; - * sysio::check(itr->account_name == name("dan"), "Lock arf, Incorrect Second Record"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - const_reverse_iterator crend()const { return std::make_reverse_iterator(cbegin()); } - - /** - * Returns an iterator pointing to the `object_type` with the lowest primary key value in the Multi-Index table. - * @ingroup multiindex - * - * @return An iterator pointing to the `object_type` with the lowest primary key value in the Multi-Index table. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * // add additional account - brendan - * - * addresses.emplace(payer, [&](auto& address) { - * address.account_name = "brendan"_n; - * address.first_name = "Brendan"; - * address.last_name = "Blumer"; - * address.street = "1 SYS Way"; - * address.city = "Hong Kong"; - * address.state = "HK"; - * }); - * auto itr = addresses.rend(); - * itr--; - * sysio::check(itr->account_name == name("brendan"), "Lock arf, Incorrect First Record "); - * itr--; - * sysio::check(itr->account_name == name("dan"), "Lock arf, Incorrect Second Record"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - const_reverse_iterator rend()const { return crend(); } - - /** - * Searches for the `object_type` with the lowest primary key that is greater than or equal to a given primary key. - * @ingroup multiindex - * - * @param primary - Primary key that establishes the target value for the lower bound search. - * @return An iterator pointing to the `object_type` that has the lowest primary key that is greater than or equal to `primary`. If an object could not be found, or if the table does not exist**, it will return the `end` iterator. - * - * Example: - * - * @code - * // This assumes the code from the get_index() example below. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * // add additional account - brendan - * - * addresses.emplace(payer, [&](auto& address) { - * address.account_name = "brendan"_n; - * address.first_name = "Brendan"; - * address.last_name = "Blumer"; - * address.street = "1 SYS Way"; - * address.city = "Hong Kong"; - * address.state = "HK"; - * address.zip = 93445; - * }); - * uint32_t zipnumb = 93445; - * auto zip_index = addresses.get_index(); - * auto itr = zip_index.lower_bound(zipnumb); - * sysio::check(itr->account_name == name("brendan"), "Lock arf, Incorrect First Lower Bound Record "); - * itr++; - * sysio::check(itr->account_name == name("dan"), "Lock arf, Incorrect Second Lower Bound Record"); - * itr++; - * sysio::check(itr == zip_index.end(), "Lock arf, Incorrect End of Iterator"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - template - const_iterator lower_bound( PK primary )const { - uint64_t primary_int = _multi_index_detail::to_raw_key(primary); - auto itr = internal_use_do_not_use::db_lowerbound_i64( _code.value, _scope, static_cast(TableName), primary_int ); - if( itr < 0 ) return end(); - const auto& obj = load_object_by_primary_iterator( itr ); - return {this, &obj}; - } - - /** - * Searches for the `object_type` with the lowest primary key that is greater than a given primary key. - * @ingroup multiindex - * - * @param primary - Primary key that establishes the target value for the upper bound search - * @return An iterator pointing to the `object_type` that has the lowest primary key that is greater than a given `primary` key. If an object could not be found, or if the table does not exist**, it will return the `end` iterator. - * - * Example: - * - * @code - * // This assumes the code from the get_index() example below. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * // add additional account - brendan - * - * addresses.emplace(payer, [&](auto& address) { - * address.account_name = "brendan"_n; - * address.first_name = "Brendan"; - * address.last_name = "Blumer"; - * address.street = "1 SYS Way"; - * address.city = "Hong Kong"; - * address.state = "HK"; - * address.zip = 93445; - * }); - * uint32_t zipnumb = 93445; - * auto zip_index = addresses.get_index(); - * auto itr = zip_index.upper_bound(zipnumb); - * sysio::check(itr->account_name == name("dan"), "Lock arf, Incorrect First Upper Bound Record "); - * itr++; - * sysio::check(itr == zip_index.end(), "Lock arf, Incorrect End of Iterator"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - template - const_iterator upper_bound( PK primary )const { - uint64_t primary_int = _multi_index_detail::to_raw_key(primary); - auto itr = internal_use_do_not_use::db_upperbound_i64( _code.value, _scope, static_cast(TableName), primary_int ); - if( itr < 0 ) return end(); - const auto& obj = load_object_by_primary_iterator( itr ); - return {this, &obj}; - } - - /** - * Returns an available primary key. - * @ingroup multiindex - * - * @return An available (unused) primary key value. - * - * Notes: - * Intended to be used in tables in which the primary keys of the table are strictly intended to be auto-incrementing, and thus will never be set to custom values by the contract. Violating this expectation could result in the table appearing to be full due to inability to allocate an available primary key. - * Ideally this method would only be used to determine the appropriate primary key to use within new objects added to a table in which the primary keys of the table are strictly intended from the beginning to be autoincrementing and thus will not ever be set to custom arbitrary values by the contract. Violating this agreement could result in the table appearing full when in reality there is plenty of space left. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * address_index addresses(_self, _self.value); // code, scope - * // add to table, first argument is account to bill for storage - * addresses.emplace(payer, [&](auto& address) { - * address.key = addresses.available_primary_key(); - * address.first_name = "Daniel"; - * address.last_name = "Larimer"; - * address.street = "1 SYS Way"; - * address.city = "Blacksburg"; - * address.state = "VA"; - * }); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - uint64_t available_primary_key()const { - if( _next_primary_key == unset_next_primary_key ) { - // This is the first time available_primary_key() is called for this multi_index instance. - if( begin() == end() ) { // empty table - _next_primary_key = 0; - } else { - auto itr = --end(); // last row of table sorted by primary key - auto pk = itr->primary_key(); // largest primary key currently in table - if( pk >= no_available_primary_key ) // Reserve the tags - _next_primary_key = no_available_primary_key; - else - _next_primary_key = pk + 1; - } - } - - sysio::check( _next_primary_key < no_available_primary_key, "next primary key in table is at autoincrement limit"); - return _next_primary_key; - } - - /** - * Returns an appropriately typed Secondary Index. - * @ingroup multiindex - * - * @tparam IndexName - the ID of the desired secondary index - * - * @return An index of the appropriate type: Primitive 64-bit unsigned integer key (idx64), Primitive 128-bit unsigned integer key (idx128), 128-bit fixed-size lexicographical key (idx128), 256-bit fixed-size lexicographical key (idx256), Floating point key, Double precision floating point key, Long Double (quadruple) precision floating point key - * - * Example: - * - * @code - * #include - * using namespace sysio; - * using namespace std; - * class addressbook: contract { - * struct address { - * uint64_t account_name; - * string first_name; - * string last_name; - * string street; - * string city; - * string state; - * uint32_t zip = 0; - * uint64_t primary_key() const { return account_name; } - * uint64_t by_zip() const { return zip; } - * }; - * public: - * addressbook(name receiver, name code, datastream ds):contract(receiver, code, ds) {} - * typedef sysio::multi_index< name("address"), address, indexed_by< name("zip"), const_mem_fun > address_index; - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * uint32_t zipnumb = 93446; - * auto zip_index = addresses.get_index(); - * auto itr = zip_index.find(zipnumb); - * sysio::check(itr->account_name == name("dan"), "Lock arf, Incorrect Record "); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - template - auto get_index() { - using namespace _multi_index_detail; - - 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 std::tuple_element_t>(this); - } - - /** - * Returns an appropriately typed Secondary Index. - * @ingroup multiindex - * - * @tparam IndexName - the ID of the desired secondary index - * - * @return An index of the appropriate type: Primitive 64-bit unsigned integer key (idx64), Primitive 128-bit unsigned integer key (idx128), 128-bit fixed-size lexicographical key (idx128), 256-bit fixed-size lexicographical key (idx256), Floating point key, Double precision floating point key, Long Double (quadruple) precision floating point key - * - * Example: - * - * @code - * // This assumes the code from the get_index() example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * // add additional account - brendan - * - * addresses.emplace(payer, [&](auto& address) { - * address.account_name = "brendan"_n; - * address.first_name = "Brendan"; - * address.last_name = "Blumer"; - * address.street = "1 SYS Way"; - * address.city = "Hong Kong"; - * address.state = "HK"; - * address.zip = 93445; - * }); - * uint32_t zipnumb = 93445; - * auto zip_index = addresses.get_index(); - * auto itr = zip_index.upper_bound(zipnumb); - * sysio::check(itr->account_name == name("dan"), "Lock arf, Incorrect First Upper Bound Record "); - * itr++; - * sysio::check(itr == zip_index.end(), "Lock arf, Incorrect End of Iterator"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - template - auto get_index()const { - using namespace _multi_index_detail; - - 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 std::tuple_element_t>(this); - } - - /** - * Returns an iterator to the given object in a Multi-Index table. - * @ingroup multiindex - * - * @param obj - A reference to the desired object - * - * @return An iterator to the given object - * - * Example: - * - * @code - * // This assumes the code from the get_index() example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example below - * // add dan account to table - see emplace example below - * // add additional account - brendan - * - * addresses.emplace(payer, [&](auto& address) { - * address.account_name = "brendan"_n; - * address.first_name = "Brendan"; - * address.last_name = "Blumer"; - * address.street = "1 SYS Way"; - * address.city = "Hong Kong"; - * address.state = "HK"; - * address.zip = 93445; - * }); - * auto user = addresses.get("dan"_n); - * auto itr = address.find("dan"_n); - * sysio::check(iterator_to(user) == itr, "Invalid iterator"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - * - * Warning: the interator_to can have undefined behavior if the caller - * passes in a reference to a stack-allocated object rather than the - * reference returned by get or by dereferencing a const_iterator. - */ - const_iterator iterator_to( const T& obj )const { - const auto& objitem = static_cast(obj); - sysio::check( objitem.__idx == this, "object passed to iterator_to is not in multi_index" ); - return {this, &objitem}; - } - /** - * Adds a new object (i.e., row) to the table. - * @ingroup multiindex - * - * @param payer - Account name of the payer for the Storage usage of the new object - * @param constructor - Lambda function that does an in-place initialization of the object to be created in the table - * - * @pre A multi index table has been instantiated - * @post A new object is created in the Multi-Index table, with a unique primary key (as specified in the object). The object is serialized and written to the table. If the table does not exist, it is created. - * @post Secondary indices are updated to refer to the newly added object. If the secondary index tables do not exist, they are created. - * @post The payer is charged for the storage usage of the new object and, if the table (and secondary index tables) must be created, for the overhead of the table creation. - * - * @return A primary key iterator to the newly created object - * - * Exception - The account is not authorized to write to the table. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * address_index addresses(_self, _self.value); // code, scope - * // add to table, first argument is account to bill for storage - * addresses.emplace(_self, [&](auto& address) { - * address.account_name = "dan"_n; - * address.first_name = "Daniel"; - * address.last_name = "Larimer"; - * address.street = "1 SYS Way"; - * address.city = "Blacksburg"; - * address.state = "VA"; - * }); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - template - const_iterator emplace( name payer, Lambda&& constructor ) { - using namespace _multi_index_detail; - - sysio::check( _code == current_receiver(), "cannot create 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 itm = std::make_unique( this, [&]( auto& i ){ - T& obj = static_cast(i); - constructor( obj ); - - size_t size = pack_size( obj ); - - //using malloc/free here potentially is not exception-safe, although WASM doesn't support exceptions - void* buffer = max_stack_buffer_size < size ? malloc(size) : alloca(size); - - datastream ds( (char*)buffer, size ); - ds << obj; - - uint64_t pk = _multi_index_detail::to_raw_key(obj.primary_key()); - - i.__primary_itr = internal_use_do_not_use::db_store_i64( _scope, static_cast(TableName), payer.value, pk, buffer, size ); - - if ( max_stack_buffer_size < size ) { - free(buffer); - } - - if( pk >= _next_primary_key ) - _next_primary_key = (pk >= no_available_primary_key) ? no_available_primary_key : (pk + 1); - - 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) ); - }); - }); - - const item* ptr = itm.get(); - auto pk = _multi_index_detail::to_raw_key(itm->primary_key()); - auto pitr = itm->__primary_itr; - - _items_vector.emplace_back( std::move(itm), pk, pitr ); - - return {this, ptr}; - } - - /** - * Modifies an existing object in a table. - * @ingroup multiindex - * - * @param itr - an iterator pointing to the object to be updated - * @param payer - account name of the payer for the storage usage of the updated row - * @param updater - lambda function that updates the target object - * - * @pre itr points to an existing element - * @pre payer is a valid account that is authorized to execute the action and be billed for storage usage. - * - * @post The modified object is serialized, then replaces the existing object in the table. - * @post Secondary indices are updated; the primary key of the updated object is not changed. - * @post The payer is charged for the storage usage of the updated object. - * @post If payer is the same as the existing payer, payer only pays for the usage difference between existing and updated object (and is refunded if this difference is negative). - * @post If payer is different from the existing payer, the existing payer is refunded for the storage usage of the existing object. - * - * Exceptions: - * If called with an invalid precondition, execution is aborted. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example - * // add dan account to table - see emplace example - * - * auto itr = addresses.find("dan"_n); - * sysio::check(itr != addresses.end(), "Address for account not found"); - * addresses.modify( itr, account payer, [&]( auto& address ) { - * address.city = "San Luis Obispo"; - * address.state = "CA"; - * }); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - template - void modify( const_iterator itr, name payer, Lambda&& updater ) { - sysio::check( itr != end(), "cannot pass end iterator to modify" ); - - modify( *itr, payer, std::forward(updater) ); - } - - /** - * Modifies an existing object in a table. - * @ingroup multiindex - * - * @param obj - a reference to the object to be updated - * @param payer - account name of the payer for the storage usage of the updated row - * @param updater - lambda function that updates the target object - * - * @pre obj is an existing object in the table - * @pre payer is a valid account that is authorized to execute the action and be billed for storage usage. - * - * @post The modified object is serialized, then replaces the existing object in the table. - * @post Secondary indices are updated; the primary key of the updated object is not changed. - * @post The payer is charged for the storage usage of the updated object. - * @post If payer is the same as the existing payer, payer only pays for the usage difference between existing and updated object (and is refunded if this difference is negative). - * @post If payer is different from the existing payer, the existing payer is refunded for the storage usage of the existing object. - * - * Exceptions: - * If called with an invalid precondition, execution is aborted. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example - * // add dan account to table - see emplace example - * - * auto itr = addresses.find("dan"_n); - * sysio::check(itr != addresses.end(), "Address for account not found"); - * addresses.modify( *itr, payer, [&]( auto& address ) { - * address.city = "San Luis Obispo"; - * address.state = "CA"; - * }); - * sysio::check(itr->city == "San Luis Obispo", "Lock arf, Address not modified"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - template - void modify( const T& obj, name payer, Lambda&& updater ) { - using namespace _multi_index_detail; - - const auto& objitem = static_cast(obj); - sysio::check( objitem.__idx == this, "object passed to modify is not in multi_index" ); - auto& mutableitem = const_cast(objitem); - sysio::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_type{}, obj); - - uint64_t pk = _multi_index_detail::to_raw_key(obj.primary_key()); - - auto& mutableobj = const_cast(obj); // Do not forget the auto& otherwise it would make a copy and thus not update at all. - updater( mutableobj ); - - sysio::check( pk == _multi_index_detail::to_raw_key(obj.primary_key()), "updater cannot change primary key when modifying an object" ); - - size_t size = pack_size( obj ); - //using malloc/free here potentially is not exception-safe, although WASM doesn't support exceptions - void* buffer = max_stack_buffer_size < size ? malloc(size) : alloca(size); - - datastream ds( (char*)buffer, size ); - ds << obj; - - internal_use_do_not_use::db_update_i64( objitem.__primary_itr, payer.value, buffer, size ); - - if ( max_stack_buffer_size < size ) { - free( buffer ); - } - - if( pk >= _next_primary_key ) - _next_primary_key = (pk >= no_available_primary_key) ? no_available_primary_key : (pk + 1); - - 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()]; - - if( indexitr < 0 ) { - typename index_type::secondary_key_type temp_secondary_key; - indexitr = mutableitem.__iters[index_type::number()] - = secondary_index_db_functions::db_idx_find_primary( _code.value, _scope, index_type::name(), pk, temp_secondary_key ); - } - - secondary_index_db_functions::db_idx_update( indexitr, payer.value, secondary ); - } - } ); - } - - /** - * Retrieves an existing object from a table using its primary key. - * @ingroup multiindex - * - * @param primary - Primary key value of the object. - * @return A constant reference to the object containing the specified primary key. - * - * Exception - No object matches the given key. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example - * // add dan account to table - see emplace example - * - * auto& user = addresses.get("dan"_n); - * sysio::check(user.first_name == "Daniel", "Couldn't get him."); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - * - * Warning: - * - * Avoid the common pitfall of copy-assigning the T& reference returned - * to a stack-allocated local variable and then passing that into modify of the multi-index. - * The most common mistake is when the local variable is defined as auto - * typename, instead it should be of type auto& or decltype(auto). - */ - template - const T& get( PK primary, const char* error_msg = "unable to find key" )const { - auto result = find( primary ); - sysio::check( result != cend(), error_msg ); - return *result; - } - - /** - * Search for an existing object in a table using its primary key. - * @ingroup multiindex - * - * @param primary - Primary key value of the object - * @return An iterator to the found object which has a primary key equal to `primary` OR the `end` iterator of the referenced table if an object with primary key `primary` is not found. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example - * // add dan account to table - see emplace example - * - * auto itr = addresses.find("dan"_n); - * sysio::check(itr != addresses.end(), "Couldn't get him."); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - template - const_iterator find( PK primary )const { - auto itr2 = std::find_if(_items_vector.rbegin(), _items_vector.rend(), [&](const item_ptr& ptr) { - return ptr._item->primary_key() == primary; - }); - if( itr2 != _items_vector.rend() ) - return iterator_to(*(itr2->_item)); - - uint64_t primary_int = _multi_index_detail::to_raw_key(primary); - auto itr = internal_use_do_not_use::db_find_i64( _code.value, _scope, static_cast(TableName), primary_int ); - if( itr < 0 ) return end(); - - const item& i = load_object_by_primary_iterator( itr ); - return iterator_to(static_cast(i)); - } - - /** - * Search for an existing object in a table using its primary key. - * @ingroup multiindex - * - * @param primary - Primary key value of the object - * @param error_msg - error message if an object with primary key `primary` is not found. - * @return An iterator to the found object which has a primary key equal to `primary` OR throws an exception if an object with primary key `primary` is not found. - */ - - template - const_iterator require_find( PK primary, const char* error_msg = "unable to find key" )const { - auto itr2 = std::find_if(_items_vector.rbegin(), _items_vector.rend(), [&](const item_ptr& ptr) { - return ptr._item->primary_key() == primary; - }); - if( itr2 != _items_vector.rend() ) - return iterator_to(*(itr2->_item)); - - uint64_t primary_int = _multi_index_detail::to_raw_key(primary); - auto itr = internal_use_do_not_use::db_find_i64( _code.value, _scope, static_cast(TableName), primary_int ); - sysio::check( itr >= 0, error_msg ); - - const item& i = load_object_by_primary_iterator( itr ); - return iterator_to(static_cast(i)); - } - - /** - * Remove an existing object from a table using its primary key. - * @ingroup multiindex - * - * @param itr - An iterator pointing to the object to be removed - * - * @pre itr points to an existing element - * @post The object is removed from the table and all associated storage is reclaimed. - * @post Secondary indices associated with the table are updated. - * @post The existing payer for storage usage of the object is refunded for the table and secondary indices usage of the removed object, and if the table and indices are removed, for the associated overhead. - * - * @return For the signature with `const_iterator`, returns a pointer to the object following the removed object. - * - * Exceptions: - * The object to be removed is not in the table. - * The action is not authorized to modify the table. - * The given iterator is invalid. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * // create reference to address_index - see emplace example - * // add dan account to table - see emplace example - * - * auto itr = addresses.find("dan"_n); - * sysio::check(itr != addresses.end(), "Address for account not found"); - * addresses.erase( itr ); - * sysio::check(itr != addresses.end(), "Everting lock arf, Address not erased properly"); - * } - * } - * SYSIO_ABI( addressbook, (myaction) ) - * @endcode - */ - const_iterator erase( const_iterator itr ) { - sysio::check( itr != end(), "cannot pass end iterator to erase" ); - - const auto& obj = *itr; - ++itr; - - erase(obj); - - return itr; - } - - /** - * Remove an existing object from a table using its primary key. - * @ingroup multiindex - * - * @param obj - Object to be removed - * - * @pre obj is an existing object in the table - * @post The object is removed from the table and all associated storage is reclaimed. - * @post Secondary indices associated with the table are updated. - * @post The existing payer for storage usage of the object is refunded for the table and secondary indices usage of the removed object, and if the table and indices are removed, for the associated overhead. - * - * Exceptions: - * The object to be removed is not in the table. - * The action is not authorized to modify the table. - * The given iterator is invalid. - * - * Example: - * - * @code - * // This assumes the code from the constructor example. Replace myaction() {...} - * - * void myaction() { - * auto itr = addresses.find("dan"_n); - * sysio::check(itr != addresses.end(), "Record is not found"); - * addresses.erase(*itr); - * itr = addresses.find("dan"_n); - * sysio::check(itr == addresses.end(), "Record is not deleted"); - * } - * } - * SYSIO_DISPATCH( addressbook, (myaction) ) - * @endcode - */ - void erase( const T& obj ) { - using namespace _multi_index_detail; - - const auto& objitem = static_cast(obj); - sysio::check( objitem.__idx == this, "object passed to erase is not in multi_index" ); - sysio::check( _code == current_receiver(), "cannot erase 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 pk = objitem.primary_key(); - auto itr2 = std::find_if(_items_vector.rbegin(), _items_vector.rend(), [&](const item_ptr& ptr) { - return ptr._item->primary_key() == pk; - }); - - sysio::check( itr2 != _items_vector.rend(), "attempt to remove object that was not in multi_index" ); - - internal_use_do_not_use::db_remove_i64( objitem.__primary_itr ); - - 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 ) { - typename index_type::secondary_key_type secondary; - i = secondary_index_db_functions::db_idx_find_primary( _code.value, _scope, index_type::name(), objitem.primary_key(), secondary ); - } - if( i >= 0 ) - secondary_index_db_functions::db_idx_remove( i ); - }); - _items_vector.erase(--(itr2.base())); - } + // sysio::multi_index is now backed by the KV database. + // This header provides backward compatibility so existing contracts + // continue to compile with #include and sysio::multi_index<...>. + template + using multi_index = kv_multi_index; -}; -} /// sysio +} // namespace sysio diff --git a/libraries/sysiolib/contracts/sysio/singleton.hpp b/libraries/sysiolib/contracts/sysio/singleton.hpp index bc1b0548e..1561042b6 100644 --- a/libraries/sysiolib/contracts/sysio/singleton.hpp +++ b/libraries/sysiolib/contracts/sysio/singleton.hpp @@ -1,133 +1,13 @@ #pragma once -#include "multi_index.hpp" -#include "system.hpp" -namespace sysio { +#include - /** - * @defgroup singleton Singleton Table - * @ingroup contracts - * @brief Defines SYSIO Singleton Table used with %multiindex - */ +namespace sysio { - /** - * This wrapper uses a single table to store named objects various types. - * - * @ingroup singleton - * @tparam SingletonName - the name of this singleton variable - * @tparam T - the type of the singleton - */ - template - class singleton - { - /** - * Primary key of the data inside singleton table - */ - constexpr static uint64_t pk_value = static_cast(SingletonName); + // sysio::singleton is now backed by the KV database. + // This header provides backward compatibility so existing contracts + // continue to compile with #include and sysio::singleton<...>. + template + using singleton = kv_singleton; - /** - * Structure of data inside the singleton table - */ - struct row { - /** - * Value to be stored inside the singleton table - */ - T value; - - /** - * Get primary key of the data - * - * @return uint64_t - Primary Key - */ - uint64_t primary_key() const { return pk_value; } - - SYSLIB_SERIALIZE( row, (value) ) - }; - - typedef sysio::multi_index table; - - public: - - /** - * Construct a new singleton object given the table's owner and the scope - * - * @param code - The table's owner - * @param scope - The scope of the table - */ - singleton( name code, uint64_t scope ) : _t( code, scope ) {} - - /** - * Check if the singleton table exists - * - * @return true - if exists - * @return false - otherwise - */ - bool exists() { - return _t.find( pk_value ) != _t.end(); - } - - /** - * Get the value stored inside the singleton table. Will throw an exception if it doesn't exist - * - * @brief Get the value stored inside the singleton table - * @return T - The value stored - */ - T get() { - auto itr = _t.find( pk_value ); - sysio::check( itr != _t.end(), "singleton does not exist" ); - return itr->value; - } - - /** - * Get the value stored inside the singleton table. If it doesn't exist, it will return the specified default value - * - * @param def - The default value to be returned in case the data doesn't exist - * @return T - The value stored - */ - T get_or_default( const T& def = T() ) { - auto itr = _t.find( pk_value ); - return itr != _t.end() ? itr->value : def; - } - - /** - * Get the value stored inside the singleton table. If it doesn't exist, it will create a new one with the specified default value - * - * @param bill_to_account - The account to bill for the newly created data if the data doesn't exist - * @param def - The default value to be created in case the data doesn't exist - * @return T - The value stored - */ - T get_or_create( name bill_to_account, const T& def = T() ) { - auto itr = _t.find( pk_value ); - return itr != _t.end() ? itr->value - : _t.emplace(bill_to_account, [&](row& r) { r.value = def; })->value; - } - - /** - * Set new value to the singleton table - * - * @param value - New value to be set - * @param bill_to_account - Account to pay for the new value - */ - void set( const T& value, name bill_to_account ) { - auto itr = _t.find( pk_value ); - if( itr != _t.end() ) { - _t.modify(itr, bill_to_account, [&](row& r) { r.value = value; }); - } else { - _t.emplace(bill_to_account, [&](row& r) { r.value = value; }); - } - } - - /** - * Remove the only data inside singleton table - */ - void remove( ) { - auto itr = _t.find( pk_value ); - if( itr != _t.end() ) { - _t.erase(itr); - } - } - - private: - table _t; - }; -} /// namespace sysio +} // namespace sysio diff --git a/plugins/sysio/abigen.hpp b/plugins/sysio/abigen.hpp index d0ba12999..efc9e7eaf 100644 --- a/plugins/sysio/abigen.hpp +++ b/plugins/sysio/abigen.hpp @@ -22,6 +22,7 @@ extern std::string output; namespace sysio { namespace cdt { class abigen : public generation_utils { std::set checked_actions; + std::set kv_key_structs; // structs referenced by [[sysio::kv_key]], must survive validate_struct public: using generation_utils::generation_utils; @@ -247,10 +248,6 @@ namespace sysio { namespace cdt { _abi.structs.insert(new_struct); } - std::string to_index_type( std::string t ) { - return "i64"; - } - void add_table( const clang::CXXRecordDecl* _decl ) { auto decl = clang_wrapper::wrap_decl(_decl); tables.insert(_decl); @@ -264,13 +261,84 @@ namespace sysio { namespace cdt { else { t.name = t.type; } + + // [[sysio::kv_key("struct_name")]] — resolve key struct fields into key_names/key_types + if (decl.isSysioKvKey()) { + auto key_struct_name = decl.getSysioKvKeyAttr()->getName().str(); + if (key_struct_name.empty()) { + // No argument: use standard KV key layout + t.key_names = {"table_name", "scope", "primary_key"}; + t.key_types = {"name", "name", "uint64"}; + } else { + // Search for key struct: nested types, then enclosing class + const clang::CXXRecordDecl* key_record = nullptr; + // Check nested types within the table struct + for (auto* d : _decl->decls()) { + if (auto* r = llvm::dyn_cast(d)) { + if (r->getNameAsString() == key_struct_name && r->isCompleteDefinition()) { + key_record = r; break; + } + } + } + // Check enclosing class (contract) + if (!key_record) { + if (auto* ctx = _decl->getDeclContext()) { + for (auto* d : ctx->decls()) { + if (auto* r = llvm::dyn_cast(d)) { + if (r->getNameAsString() == key_struct_name && r->isCompleteDefinition()) { + key_record = r; break; + } + } + } + } + } + + if (key_record) { + // Extract field names/types for key_names/key_types + for (auto* field : key_record->fields()) { + t.key_names.push_back(field->getName().str()); + t.key_types.push_back(translate_type(field->getType())); + } + // Add the key struct to ABI structs so clients can reference it + kv_key_structs.insert(key_struct_name); + abi_struct ks; + ks.name = key_struct_name; + for (auto* field : key_record->fields()) { + ks.fields.push_back({field->getName().str(), get_type(field->getType())}); + add_type(field->getType()); + } + _abi.structs.insert(ks); + } else { + // Fallback: check already-processed ABI structs + bool found = false; + for (const auto& s : _abi.structs) { + if (s.name == key_struct_name) { + for (const auto& f : s.fields) { + t.key_names.push_back(f.name); + t.key_types.push_back(f.type); + } + found = true; + break; + } + } + CDT_CHECK_WARN(found, "abigen_warning", _decl->getLocation(), + "kv_key struct '" + key_struct_name + "' not found; key_names/key_types will be empty in ABI"); + } + } + } + ctables.insert(t); } - void add_table( uint64_t name, const clang::CXXRecordDecl* decl ) { + void add_table( uint64_t name, const clang::CXXRecordDecl* decl, bool is_kv = false ) { abi_table t; t.type = decl->getNameAsString(); t.name = name_to_string(name); + if (is_kv) { + // KV tables use fixed 24-byte big-endian key: [table_name:8B][scope:8B][primary_key:8B] + t.key_names = {"table_name", "scope", "primary_key"}; + t.key_types = {"name", "name", "uint64"}; + } _abi.tables.insert(t); } @@ -628,7 +696,11 @@ namespace sysio { namespace cdt { o["type"] = t.type; o["index_type"] = "i64"; o["key_names"] = ojson::array(); + for (const auto& kn : t.key_names) + o["key_names"].push_back(kn); o["key_types"] = ojson::array(); + for (const auto& kt : t.key_types) + o["key_types"].push_back(kt); return o; } @@ -668,6 +740,9 @@ namespace sysio { namespace cdt { return o; } + void set_has_pre_dispatch() { _abi.has_pre_dispatch = true; } + void set_has_post_dispatch() { _abi.has_post_dispatch = true; } + bool has_wasm_data() const { return !_abi.wasm_actions.empty() || !_abi.wasm_notifies.empty() || !_abi.wasm_entries.empty(); } @@ -758,6 +833,9 @@ namespace sysio { namespace cdt { for( auto t : set_of_tables ) { if (as.name == _translate_type(t.type)) return true; + // Include structs referenced by [[sysio::kv_key]] + if (kv_key_structs.count(as.name)) + return true; } for( auto td : _abi.typedefs ) { if (as.name == _translate_type(remove_suffix(td.type))) @@ -885,6 +963,10 @@ namespace sysio { namespace cdt { for (auto& e : pb_types) { o["pb_types"].push_back(e); } + if (_abi.has_pre_dispatch) + o["has_pre_dispatch"] = true; + if (_abi.has_post_dispatch) + o["has_post_dispatch"] = true; return o; } @@ -985,13 +1067,15 @@ namespace sysio { namespace cdt { virtual bool VisitDecl(clang::Decl* decl) { if (const auto* d = dyn_cast(decl)) { - if (d->getName() == "multi_index" || d->getName() == "singleton") { + if (d->getName() == "multi_index" || d->getName() == "singleton" || + d->getName() == "kv_multi_index" || d->getName() == "table") { + bool is_kv = (d->getName() == "kv_multi_index" || d->getName() == "table"); // second template parameter is table type const auto* table_type = d->getTemplateArgs()[1].getAsType().getTypePtr()->getAsCXXRecordDecl(); auto table_decl = clang_wrapper::wrap_decl(table_type); if ((table_decl.isSysioTable() && ag.is_sysio_contract(table_decl, ag.get_contract_name())) || defined_in_contract(d)) { // first parameter is table name - ag.add_table(d->getTemplateArgs()[0].getAsIntegral().getLimitedValue(), table_type); + ag.add_table(d->getTemplateArgs()[0].getAsIntegral().getLimitedValue(), table_type, is_kv); if (table_decl.isSysioTable()) ag.add_struct(table_type); } diff --git a/plugins/sysio/clang_wrapper.hpp b/plugins/sysio/clang_wrapper.hpp index a312bfbdc..a5dabe066 100644 --- a/plugins/sysio/clang_wrapper.hpp +++ b/plugins/sysio/clang_wrapper.hpp @@ -66,6 +66,10 @@ namespace sysio_plugin { namespace clang_wrapper { return attrs.find("sysio_table") != attrs.end(); } + bool isSysioKvKey() const { + return attrs.find("sysio_kv_key") != attrs.end(); + } + bool isSysioType() const { return attrs.find("sysio_type") != attrs.end(); } @@ -103,6 +107,10 @@ namespace sysio_plugin { namespace clang_wrapper { return isSysioTable() ? &attrs.at("sysio_table") : nullptr; } + const Attr* getSysioKvKeyAttr() const { + return isSysioKvKey() ? &attrs.at("sysio_kv_key") : nullptr; + } + const Attr* getSysioTypeAttr() const { return isSysioType() ? &attrs.at("sysio_type") : nullptr; } diff --git a/plugins/sysio/sysio_attrs.cpp b/plugins/sysio/sysio_attrs.cpp index 0c84de0d4..f1b0970fe 100644 --- a/plugins/sysio/sysio_attrs.cpp +++ b/plugins/sysio/sysio_attrs.cpp @@ -86,6 +86,7 @@ SYSIO_ATTR(SysioRicardian, sysio_ricardian, sysio::ricardian, 1, 0, (!isa(D) && !isa(D))) SYSIO_ATTR(SysioAction, sysio_action, sysio::action, 0, 1, (!isa(D) && !isa(D))) SYSIO_ATTR(SysioTable, sysio_table, sysio::table, 0, 1, (!isa(D))) +SYSIO_ATTR(SysioKvKey, sysio_kv_key, sysio::kv_key, 0, 1, (!isa(D))) SYSIO_ATTR(SysioWasmAction, sysio_wasm_action, sysio::wasm_action, 0, 1, (!isa(D))) SYSIO_ATTR(SysioWasmNotify, sysio_wasm_notify, sysio::wasm_notify, 0, 1, (!isa(D))) SYSIO_ATTR(SysioWasmAbi, sysio_wasm_abi, sysio::wasm_abi, 0, 1, (!isa(D))) diff --git a/plugins/sysio/sysio_codegen.cpp b/plugins/sysio/sysio_codegen.cpp index ff1cdb744..8c96ea345 100644 --- a/plugins/sysio/sysio_codegen.cpp +++ b/plugins/sysio/sysio_codegen.cpp @@ -329,6 +329,11 @@ class sysio_codegen_visitor : public RecursiveASTVisitor, if (auto* fd = dyn_cast(decl)) { if (fd->getNameInfo().getAsString() == "apply" && _decl.isSysioWasmEntry()) abigen::get().add_wasm_entries(_decl); + if (fd->isExternC() && fd->isThisDeclarationADefinition()) { + auto name = fd->getNameInfo().getAsString(); + if (name == "pre_dispatch") abigen::get().set_has_pre_dispatch(); + if (name == "post_dispatch") abigen::get().set_has_post_dispatch(); + } } else { auto process_global_var = [this]( clang::Decl* d ) { if (auto* vd = dyn_cast(d)) { diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt index 8ec509086..05e5d7b82 100644 --- a/tests/integration/CMakeLists.txt +++ b/tests/integration/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required( VERSION 3.5 ) -set(SYSIO_VERSION_MIN "3.1") +set(SYSIO_VERSION_MIN "1.0") set(SYSIO_VERSION_SOFT_MAX "5.0") #set(SYSIO_VERSION_HARD_MAX "") diff --git a/tests/integration/action_results_test.cpp b/tests/integration/action_results_test.cpp index 7fbc5d764..fe6624ce2 100644 --- a/tests/integration/action_results_test.cpp +++ b/tests/integration/action_results_test.cpp @@ -25,14 +25,13 @@ BOOST_FIXTURE_TEST_CASE( action_results_tests, tester ) try { produce_blocks(); auto trace = push_action("test"_n, "action1"_n, "test"_n, mvo()); - // need to fix this test after Kevin fixes action_return - wdump((trace)); + BOOST_TEST_MESSAGE(fc::json::to_pretty_string(trace)); trace = push_action("test"_n, "action2"_n, "test"_n, mvo()); - wdump((trace)); + BOOST_TEST_MESSAGE(fc::json::to_pretty_string(trace)); trace = push_action("test"_n, "action3"_n, "test"_n, mvo()); - wdump((trace)); + BOOST_TEST_MESSAGE(fc::json::to_pretty_string(trace)); } FC_LOG_AND_RETHROW() diff --git a/tests/integration/contracts.hpp.in b/tests/integration/contracts.hpp.in index 4a354aae5..66a3fd71d 100644 --- a/tests/integration/contracts.hpp.in +++ b/tests/integration/contracts.hpp.in @@ -39,5 +39,14 @@ namespace sysio::testing { 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"); } + + static std::vector kv_table_tests_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/kv_table_tests.wasm"); } + static std::vector kv_table_tests_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/kv_table_tests.abi"); } + + static std::vector kv_raw_table_tests_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/kv_raw_table_tests.wasm"); } + static std::vector kv_raw_table_tests_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/kv_raw_table_tests.abi"); } + + static std::vector kv_singleton_tests_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/kv_singleton_tests.wasm"); } + static std::vector kv_singleton_tests_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/kv_singleton_tests.abi"); } }; } //ns sysio::testing diff --git a/tests/integration/instant_finality_tests.cpp b/tests/integration/instant_finality_tests.cpp index 9cd6b8f5d..ca0f3df33 100644 --- a/tests/integration/instant_finality_tests.cpp +++ b/tests/integration/instant_finality_tests.cpp @@ -35,8 +35,7 @@ BOOST_FIXTURE_TEST_CASE(instant_finality_test, tester) try { std::cout << fc::json::to_string(pretty_output, fc::time_point::now() + abi_serializer_max_time) << std::endl; std::string output_json = fc::json::to_pretty_string(pretty_output); - BOOST_TEST(output_json.find("finality_extension") != std::string::npos); - BOOST_TEST(output_json.find("\"generation\": 2") != std::string::npos); + BOOST_TEST(output_json.find("new_finalizer_policy_diff") != std::string::npos); BOOST_TEST(output_json.find("\"threshold\": 1") != std::string::npos); BOOST_TEST(output_json.find("\"description\": \"test_desc\"") != std::string::npos); BOOST_TEST(output_json.find("\"weight\": 1") != std::string::npos); diff --git a/tests/integration/kv_raw_table_tests.cpp b/tests/integration/kv_raw_table_tests.cpp new file mode 100644 index 000000000..8e51be944 --- /dev/null +++ b/tests/integration/kv_raw_table_tests.cpp @@ -0,0 +1,71 @@ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-compare" +#include +#pragma GCC diagnostic pop + +#include + +#include + +using namespace sysio; +using namespace sysio::chain; +using namespace sysio::testing; + +#ifdef NON_VALIDATING_TEST +#define TESTER tester +#else +#define TESTER validating_tester +#endif + +BOOST_AUTO_TEST_SUITE(kv_raw_table_tests) + +// Helper: deploy kv_raw_table_tests contract to a fresh account and run one action +#define KV_RAW_TABLE_TEST(acct, action_name) \ + BOOST_FIXTURE_TEST_CASE(kv_raw_table_##action_name, TESTER) { try { \ + produce_blocks(1); \ + create_account( acct ); \ + produce_blocks(1); \ + set_code( acct, contracts::kv_raw_table_tests_wasm() ); \ + set_abi( acct, contracts::kv_raw_table_tests_abi().data() ); \ + produce_blocks(1); \ + push_action( acct, #action_name ""_n, acct, {} ); \ + BOOST_REQUIRE_EQUAL( validate(), true ); \ + } FC_LOG_AND_RETHROW() } + +KV_RAW_TABLE_TEST( "kvmap1"_n, setget ) +KV_RAW_TABLE_TEST( "kvmap2"_n, erasetest ) +KV_RAW_TABLE_TEST( "kvmap3"_n, uintorder ) +KV_RAW_TABLE_TEST( "kvmap4"_n, signedorder ) +KV_RAW_TABLE_TEST( "kvmap5"_n, signed32 ) +KV_RAW_TABLE_TEST( "kvmapa"_n, signedsm ) +KV_RAW_TABLE_TEST( "kvmapb"_n, strorder ) +KV_RAW_TABLE_TEST( "kvmapc"_n, bounds ) +KV_RAW_TABLE_TEST( "kvmapd"_n, emptyiter ) +KV_RAW_TABLE_TEST( "kvmape"_n, overwrite ) +KV_RAW_TABLE_TEST( "kvmapf"_n, reviter ) +KV_RAW_TABLE_TEST( "kvmapg"_n, signededge ) +KV_RAW_TABLE_TEST( "kvmaph"_n, floatorder ) +KV_RAW_TABLE_TEST( "kvmapi"_n, dblorder ) +KV_RAW_TABLE_TEST( "kvmapj"_n, signedi ) +KV_RAW_TABLE_TEST( "kvmapk"_n, strnul ) +KV_RAW_TABLE_TEST( "kvmapl"_n, blobkey ) +KV_RAW_TABLE_TEST( "kvmapn"_n, crossread ) +KV_RAW_TABLE_TEST( "kvmapo"_n, zeroval ) +KV_RAW_TABLE_TEST( "kvmapp"_n, ramdelta ) + +// Negative test: erase non-existent key should assert (T1/T7) +BOOST_FIXTURE_TEST_CASE(kv_raw_table_erasebad, TESTER) { try { + produce_blocks(1); + create_account( "kvmapm"_n ); + produce_blocks(1); + set_code( "kvmapm"_n, contracts::kv_raw_table_tests_wasm() ); + set_abi( "kvmapm"_n, contracts::kv_raw_table_tests_abi().data() ); + produce_blocks(1); + BOOST_CHECK_EXCEPTION( + push_action( "kvmapm"_n, "erasebad"_n, "kvmapm"_n, {} ), + kv_key_not_found, + fc_exception_message_is("KV key not found for erase") + ); +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/integration/kv_singleton_tests.cpp b/tests/integration/kv_singleton_tests.cpp new file mode 100644 index 000000000..614b656af --- /dev/null +++ b/tests/integration/kv_singleton_tests.cpp @@ -0,0 +1,54 @@ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-compare" +#include +#pragma GCC diagnostic pop + +#include + +#include + +using namespace sysio; +using namespace sysio::testing; + +#ifdef NON_VALIDATING_TEST +#define TESTER tester +#else +#define TESTER validating_tester +#endif + +BOOST_AUTO_TEST_SUITE(kv_singleton_tests) + +BOOST_FIXTURE_TEST_CASE(kv_singleton_integration, TESTER) { try { + produce_blocks(1); + create_account( "kvsngl"_n ); + produce_blocks(1); + set_code( "kvsngl"_n, contracts::kv_singleton_tests_wasm() ); + set_abi( "kvsngl"_n, contracts::kv_singleton_tests_abi().data() ); + produce_blocks(1); + + push_action( "kvsngl"_n, "setget"_n, "kvsngl"_n, {} ); + push_action( "kvsngl"_n, "getdefault"_n, "kvsngl"_n, {} ); + push_action( "kvsngl"_n, "getorcreate"_n, "kvsngl"_n, {} ); + push_action( "kvsngl"_n, "removetest"_n, "kvsngl"_n, {} ); + push_action( "kvsngl"_n, "settwice"_n, "kvsngl"_n, {} ); + push_action( "kvsngl"_n, "scopetest"_n, "kvsngl"_n, {} ); + + BOOST_REQUIRE_EQUAL( validate(), true ); +} FC_LOG_AND_RETHROW() } + +// Negative test: get() on unset singleton should assert (T2) +BOOST_FIXTURE_TEST_CASE(kv_singleton_getunset, TESTER) { try { + produce_blocks(1); + create_account( "kvsngl2"_n ); + produce_blocks(1); + set_code( "kvsngl2"_n, contracts::kv_singleton_tests_wasm() ); + set_abi( "kvsngl2"_n, contracts::kv_singleton_tests_abi().data() ); + produce_blocks(1); + BOOST_CHECK_EXCEPTION( + push_action( "kvsngl2"_n, "getunset"_n, "kvsngl2"_n, {} ), + sysio_assert_message_exception, + sysio_assert_message_is("singleton does not exist") + ); +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/integration/kv_table_tests.cpp b/tests/integration/kv_table_tests.cpp new file mode 100644 index 000000000..bdca6eeda --- /dev/null +++ b/tests/integration/kv_table_tests.cpp @@ -0,0 +1,97 @@ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-compare" +#include +#pragma GCC diagnostic pop + +#include + +#include + +using namespace sysio; +using namespace sysio::chain; +using namespace sysio::testing; + +#ifdef NON_VALIDATING_TEST +#define TESTER tester +#else +#define TESTER validating_tester +#endif + +BOOST_AUTO_TEST_SUITE(kv_table_tests) + +BOOST_FIXTURE_TEST_CASE(kv_table_integration, TESTER) { try { + produce_blocks(1); + create_account( "kvtest"_n ); + produce_blocks(1); + set_code( "kvtest"_n, contracts::kv_table_tests_wasm() ); + set_abi( "kvtest"_n, contracts::kv_table_tests_abi().data() ); + produce_blocks(1); + + push_action( "kvtest"_n, "emplacefind"_n, "kvtest"_n, {} ); + push_action( "kvtest"_n, "modify"_n, "kvtest"_n, {} ); + push_action( "kvtest"_n, "erase"_n, "kvtest"_n, {} ); + push_action( "kvtest"_n, "iterate"_n, "kvtest"_n, {} ); + push_action( "kvtest"_n, "lowerupper"_n, "kvtest"_n, {} ); + push_action( "kvtest"_n, "getbyval"_n, "kvtest"_n, {} ); + push_action( "kvtest"_n, "reqfind"_n, "kvtest"_n, {} ); + push_action( "kvtest"_n, "availpk"_n, "kvtest"_n, {} ); + // crossscope moved to its own test case with fresh account (below) + push_action( "kvtest"_n, "emptyiter"_n, "kvtest"_n, {} ); + push_action( "kvtest"_n, "constiter"_n, "kvtest"_n, {} ); + push_action( "kvtest"_n, "erasebypk"_n, "kvtest"_n, {} ); + push_action( "kvtest"_n, "setmethod"_n, "kvtest"_n, {} ); + push_action( "kvtest"_n, "modifyobj"_n, "kvtest"_n, {} ); + push_action( "kvtest"_n, "endallscope"_n, "kvtest"_n, {} ); + + BOOST_CHECK_EXCEPTION( + push_action( "kvtest"_n, "reqfindfail"_n, "kvtest"_n, {} ), + sysio_assert_message_exception, + sysio_assert_message_is( "expected failure" ) + ); + + BOOST_REQUIRE_EQUAL( validate(), true ); +} FC_LOG_AND_RETHROW() } + +// Cross-scope iteration on a fresh account (exact count check) +BOOST_FIXTURE_TEST_CASE(kv_table_crossscope, TESTER) { try { + produce_blocks(1); + create_account( "kvscope"_n ); + produce_blocks(1); + set_code( "kvscope"_n, contracts::kv_table_tests_wasm() ); + set_abi( "kvscope"_n, contracts::kv_table_tests_abi().data() ); + produce_blocks(1); + push_action( "kvscope"_n, "crossscope"_n, "kvscope"_n, {} ); + BOOST_REQUIRE_EQUAL( validate(), true ); +} FC_LOG_AND_RETHROW() } + +// Negative: erase non-existent pk (T6) +BOOST_FIXTURE_TEST_CASE(kv_table_erasebadpk, TESTER) { try { + produce_blocks(1); + create_account( "kvtest2"_n ); + produce_blocks(1); + set_code( "kvtest2"_n, contracts::kv_table_tests_wasm() ); + set_abi( "kvtest2"_n, contracts::kv_table_tests_abi().data() ); + produce_blocks(1); + BOOST_CHECK_EXCEPTION( + push_action( "kvtest2"_n, "erasebadpk"_n, "kvtest2"_n, {} ), + kv_key_not_found, + fc_exception_message_is( "KV key not found for erase" ) + ); +} FC_LOG_AND_RETHROW() } + +// Negative: modify that changes primary key (T6) +BOOST_FIXTURE_TEST_CASE(kv_table_modifypk, TESTER) { try { + produce_blocks(1); + create_account( "kvtest3"_n ); + produce_blocks(1); + set_code( "kvtest3"_n, contracts::kv_table_tests_wasm() ); + set_abi( "kvtest3"_n, contracts::kv_table_tests_abi().data() ); + produce_blocks(1); + BOOST_CHECK_EXCEPTION( + push_action( "kvtest3"_n, "modifypk"_n, "kvtest3"_n, {} ), + sysio_assert_message_exception, + sysio_assert_message_is( "cannot modify primary key" ) + ); +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/integration/main.cpp b/tests/integration/main.cpp index cb0252207..3f9e25a29 100644 --- a/tests/integration/main.cpp +++ b/tests/integration/main.cpp @@ -29,8 +29,8 @@ boost::unit_test::test_suite* init_unit_test_suite(int argc, char* argv[]) { break; } } - if(!is_verbose) fc::logger::get(DEFAULT_LOGGER).set_log_level(fc::log_level::off); - if(is_verbose) fc::logger::get(DEFAULT_LOGGER).set_log_level(fc::log_level::all); + if(!is_verbose) fc::logger::default_logger().set_log_level(fc::log_level::off); + if(is_verbose) fc::logger::default_logger().set_log_level(fc::log_level::all); // Register fc::exception translator boost::unit_test::unit_test_monitor.template register_exception_translator(&translate_fc_exception); diff --git a/tests/integration/multi_index_tests.cpp b/tests/integration/multi_index_tests.cpp index 73f320d15..a9703be5f 100644 --- a/tests/integration/multi_index_tests.cpp +++ b/tests/integration/multi_index_tests.cpp @@ -47,20 +47,24 @@ BOOST_FIXTURE_TEST_CASE(main_multi_index_tests, TESTER) { try { 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( "s1pkend"_n, "cannot increment end iterator" ); + check_failure( "s1skend"_n, "cannot increment end iterator" ); + check_failure( "s1pkbegin"_n, "cannot decrement iterator at beginning of table" ); + check_failure( "s1skbegin"_n, "cannot decrement iterator at beginning of index" ); + check_failure( "s1pkref"_n, "object passed to iterator_to is not in multi_index" ); + // KV: secondary iterator_to populates cache via find, so cross-table + // detection is not possible. Dev safety check only, no data impact. + push_action( "testapi"_n, "s1skref"_n, "testapi"_n, {} ); + check_failure( "s1pkitrto"_n, "dereferencing invalid iterator" ); // deref end fires before iterator_to + check_failure( "s1pkmodify"_n, "cannot pass end iterator to modify" ); + check_failure( "s1pkerase"_n, "cannot pass end iterator to erase" ); + check_failure( "s1skitrto"_n, "deref invalid sec iter" ); // deref end fires before iterator_to + check_failure( "s1skmodify"_n, "cannot pass end iterator to modify" ); + check_failure( "s1skerase"_n, "cannot pass end iterator to erase" ); + check_failure( "s1modpk"_n, "updater cannot change primary key when modifying an object" ); + // KV: autoincrement limit only triggers when max_key exists in storage, + // not through in-memory cache path. Dev safety check only, no data impact. + push_action( "testapi"_n, "s1exhaustpk"_n, "testapi"_n, {} ); 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 @@ -69,6 +73,16 @@ BOOST_FIXTURE_TEST_CASE(main_multi_index_tests, TESTER) { try { 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 + // secondary iterator edge cases + push_action( "testapi"_n, "s1clone"_n, "testapi"_n, {} ); // sec iterator clone with duplicate keys + push_action( "testapi"_n, "s1secrb"_n, "testapi"_n, {} ); // sec rbegin/rend (uint64_t) + push_action( "testapi"_n, "s2secrb"_n, "testapi"_n, {} ); // sec rbegin/rend (uint128_t) + push_action( "testapi"_n, "namepk"_n, "testapi"_n, {} ); // name-typed primary key + push_action( "testapi"_n, "cbegincend"_n, "testapi"_n, {} ); // cbegin/cend + push_action( "testapi"_n, "codescope"_n, "testapi"_n, {} ); // get_code/get_scope + push_action( "testapi"_n, "crbeginend"_n, "testapi"_n, {} ); // crbegin/crend + push_action( "testapi"_n, "s1secupd"_n, "testapi"_n, {} ); // T3: kv_idx_update verification + BOOST_REQUIRE_EQUAL( validate(), true ); } FC_LOG_AND_RETHROW() } diff --git a/tests/toolchain/abigen-pass/exclude_from_abi.hpp b/tests/toolchain/abigen-pass/exclude_from_abi.hpp index a0abf7a3f..ea3cc3d6f 100644 --- a/tests/toolchain/abigen-pass/exclude_from_abi.hpp +++ b/tests/toolchain/abigen-pass/exclude_from_abi.hpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include @@ -11,8 +11,8 @@ struct [[sysio::table]] out_of_class { uint64_t id; uint64_t primary_key() const { return id; } }; -typedef sysio::multi_index<"mi.config55"_n, out_of_class> out_of_class_index; -using uout_of_class_index = sysio::multi_index<"mi.config551"_n, out_of_class>; +typedef sysio::kv_multi_index<"mi.config55"_n, out_of_class> out_of_class_index; +using uout_of_class_index = sysio::kv_multi_index<"mi.config551"_n, out_of_class>; typedef sysio::singleton<"smpl.conf55"_n, sysio::name> smpl_config55; typedef sysio::singleton<"config55"_n, out_of_class> config55; @@ -45,6 +45,6 @@ class [[sysio::contract("singleton_contract_simple2")]] singleton_contract_simpl - typedef sysio::multi_index<"mi.config553"_n, inside_class> inside_class_index; - using uinside_class_index = sysio::multi_index<"mi.config554"_n, inside_class>; + typedef sysio::kv_multi_index<"mi.config553"_n, inside_class> inside_class_index; + using uinside_class_index = sysio::kv_multi_index<"mi.config554"_n, inside_class>; }; diff --git a/tests/toolchain/abigen-pass/kv_key_types.abi b/tests/toolchain/abigen-pass/kv_key_types.abi new file mode 100644 index 000000000..b60deb830 --- /dev/null +++ b/tests/toolchain/abigen-pass/kv_key_types.abi @@ -0,0 +1,76 @@ +{ + "____comment": "This file was generated with sysio-abigen. DO NOT EDIT ", + "version": "sysio::abi/1.2", + "types": [], + "structs": [ + { + "name": "kvrow", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "data", + "type": "string" + } + ] + }, + { + "name": "my_key", + "base": "", + "fields": [ + { + "name": "region", + "type": "string" + }, + { + "name": "id", + "type": "uint64" + } + ] + }, + { + "name": "my_value", + "base": "", + "fields": [ + { + "name": "payload", + "type": "string" + } + ] + }, + { + "name": "test", + "base": "", + "fields": [] + } + ], + "actions": [ + { + "name": "test", + "type": "test", + "ricardian_contract": "" + } + ], + "tables": [ + { + "name": "custom", + "type": "my_value", + "index_type": "i64", + "key_names": ["region","id"], + "key_types": ["string","uint64"] + }, + { + "name": "kvrows", + "type": "kvrow", + "index_type": "i64", + "key_names": ["table_name","scope","primary_key"], + "key_types": ["name","name","uint64"] + } + ], + "ricardian_clauses": [], + "variants": [], + "action_results": [] +} \ No newline at end of file diff --git a/tests/toolchain/abigen-pass/kv_key_types.cpp b/tests/toolchain/abigen-pass/kv_key_types.cpp new file mode 100644 index 000000000..d8be3b102 --- /dev/null +++ b/tests/toolchain/abigen-pass/kv_key_types.cpp @@ -0,0 +1,34 @@ +#include +#include + +using namespace sysio; + +// Test: kv_multi_index tables get auto key metadata, +// and [[sysio::kv_key]] provides custom key metadata. +class [[sysio::contract("kv_key_types")]] kv_key_types : public contract { + public: + using contract::contract; + + // Auto key metadata via kv_multi_index template + struct [[sysio::table]] kvrow { + uint64_t id; + std::string data; + uint64_t primary_key() const { return id; } + }; + typedef kv_multi_index<"kvrows"_n, kvrow> kvrows_table; + + // Custom key metadata via [[sysio::kv_key]] + struct my_key { + std::string region; + uint64_t id; + SYSLIB_SERIALIZE(my_key, (region)(id)) + }; + + struct [[sysio::table("custom"), sysio::kv_key("my_key")]] my_value { + std::string payload; + SYSLIB_SERIALIZE(my_value, (payload)) + }; + + [[sysio::action]] + void test() {}; +}; diff --git a/tests/toolchain/abigen-pass/kv_key_types.json b/tests/toolchain/abigen-pass/kv_key_types.json new file mode 100644 index 000000000..dcb246424 --- /dev/null +++ b/tests/toolchain/abigen-pass/kv_key_types.json @@ -0,0 +1,9 @@ +{ + "tests" : [ + { + "expected" : { + "abi-file" : "kv_key_types.abi" + } + } + ] + } diff --git a/tests/toolchain/abigen-pass/singleton_contract.abi b/tests/toolchain/abigen-pass/singleton_contract.abi index 070ae4be4..d6b557ab8 100644 --- a/tests/toolchain/abigen-pass/singleton_contract.abi +++ b/tests/toolchain/abigen-pass/singleton_contract.abi @@ -4,7 +4,7 @@ "types": [], "structs": [ { - "name": "out_of_class", + "name": "out_of_class3", "base": "", "fields": [ { @@ -13,16 +13,6 @@ } ] }, - { - "name": "out_of_class3", - "base": "", - "fields": [ - { - "name": "id", - "type": "uint64" - } - ] - }, { "name": "tbl_config", "base": "", @@ -51,37 +41,16 @@ } ], "tables": [ - { - "name": "config", - "type": "tbl_config", - "index_type": "i64", - "key_names": [], - "key_types": [] - }, - { - "name": "config55", - "type": "out_of_class", - "index_type": "i64", - "key_names": [], - "key_types": [] - }, { "name": "mi.config52", "type": "out_of_class3", "index_type": "i64", - "key_names": [], - "key_types": [] - }, - { - "name": "smpl.conf5", - "type": "name", - "index_type": "i64", - "key_names": [], - "key_types": [] + "key_names": ["table_name","scope","primary_key"], + "key_types": ["name","name","uint64"] }, { - "name": "smpl.config", - "type": "name", + "name": "tbl_config", + "type": "tbl_config", "index_type": "i64", "key_names": [], "key_types": [] diff --git a/tests/toolchain/abigen-pass/singleton_contract.cpp b/tests/toolchain/abigen-pass/singleton_contract.cpp index 139c9da42..432c02430 100644 --- a/tests/toolchain/abigen-pass/singleton_contract.cpp +++ b/tests/toolchain/abigen-pass/singleton_contract.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "exclude_from_abi.hpp" @@ -11,14 +12,14 @@ struct [[sysio::table]] out_of_class2 { uint64_t id; uint64_t primary_key() const { return id; } }; -typedef sysio::multi_index<"mi.config5"_n, out_of_class2> out_of_class_index51; -using uout_of_class_index51 = sysio::multi_index<"mi.config51"_n, out_of_class2>; +typedef sysio::kv_multi_index<"mi.config5"_n, out_of_class2> out_of_class_index51; +using uout_of_class_index51 = sysio::kv_multi_index<"mi.config51"_n, out_of_class2>; struct [[sysio::table, sysio::contract("singleton_contract")]] out_of_class3 { uint64_t id; uint64_t primary_key() const { return id; } }; -typedef sysio::multi_index<"mi.config52"_n, out_of_class3> out_of_class_index52; +typedef sysio::kv_multi_index<"mi.config52"_n, out_of_class3> out_of_class_index52; typedef sysio::singleton<"smpl.conf5"_n, sysio::name> smpl_config5; typedef sysio::singleton<"config5"_n, out_of_class2> config5; diff --git a/tests/toolchain/abigen-pass/using_std_array.abi b/tests/toolchain/abigen-pass/using_std_array.abi index 3a42ec82e..ded5e8745 100644 --- a/tests/toolchain/abigen-pass/using_std_array.abi +++ b/tests/toolchain/abigen-pass/using_std_array.abi @@ -40,8 +40,8 @@ "name": "greeting", "type": "greeting", "index_type": "i64", - "key_names": [], - "key_types": [] + "key_names": ["table_name","scope","primary_key"], + "key_types": ["name","name","uint64"] } ], "ricardian_clauses": [], diff --git a/tests/toolchain/abigen-pass/using_std_array.cpp b/tests/toolchain/abigen-pass/using_std_array.cpp index 11fbf1c8d..befb6f734 100644 --- a/tests/toolchain/abigen-pass/using_std_array.cpp +++ b/tests/toolchain/abigen-pass/using_std_array.cpp @@ -1,5 +1,6 @@ #include #include +#include #include using std::array; @@ -20,5 +21,5 @@ class[[sysio::contract("using_std_array")]] using_std_array : public contract array t; uint64_t primary_key() const { return id; } }; - typedef multi_index<"greeting"_n, greeting> greeting_index; + typedef kv_multi_index<"greeting"_n, greeting> greeting_index; }; diff --git a/tests/toolchain/compile-fail/host_functions_tests.cpp b/tests/toolchain/compile-fail/host_functions_tests.cpp index 2862e809c..bcfde09c3 100644 --- a/tests/toolchain/compile-fail/host_functions_tests.cpp +++ b/tests/toolchain/compile-fail/host_functions_tests.cpp @@ -28,25 +28,12 @@ extern "C" __attribute__((sysio_wasm_import)) void set_resource_limit(int64_t, i extern "C" __attribute__((sysio_wasm_import)) void set_blockchain_parameters_packed( char* data, uint32_t datalen ); extern "C" __attribute__((sysio_wasm_import)) uint32_t get_blockchain_parameters_packed( char* data, uint32_t datalen ); -typedef uint64_t capi_name; -extern "C" __attribute__((sysio_wasm_import)) int32_t db_store_i64(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const void* data, uint32_t len); -extern "C" __attribute__((sysio_wasm_import)) void db_update_i64(int32_t iterator, capi_name payer, const void* data, uint32_t len); -extern "C" __attribute__((sysio_wasm_import)) void db_remove_i64(int32_t iterator); -extern "C" __attribute__((sysio_wasm_import)) int32_t db_idx64_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const uint64_t* secondary); -extern "C" __attribute__((sysio_wasm_import)) void db_idx64_update(int32_t iterator, capi_name payer, const uint64_t* secondary); -extern "C" __attribute__((sysio_wasm_import)) void db_idx64_remove(int32_t iterator); -extern "C" __attribute__((sysio_wasm_import)) int32_t db_idx128_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const uint128_t* secondary); -extern "C" __attribute__((sysio_wasm_import)) void db_idx128_update(int32_t iterator, capi_name payer, const uint128_t* secondary); -extern "C" __attribute__((sysio_wasm_import)) void db_idx128_remove(int32_t iterator); -extern "C" __attribute__((sysio_wasm_import)) int32_t db_idx256_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const uint128_t* data, uint32_t data_len ); -extern "C" __attribute__((sysio_wasm_import)) void db_idx256_update(int32_t iterator, capi_name payer, const uint128_t* data, uint32_t data_len); -extern "C" __attribute__((sysio_wasm_import)) void db_idx256_remove(int32_t iterator); -extern "C" __attribute__((sysio_wasm_import)) int32_t db_idx_double_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const double* secondary); -extern "C" __attribute__((sysio_wasm_import)) void db_idx_double_update(int32_t iterator, capi_name payer, const double* secondary); -extern "C" __attribute__((sysio_wasm_import)) void db_idx_double_remove(int32_t iterator); -extern "C" __attribute__((sysio_wasm_import)) int32_t db_idx_long_double_store(uint64_t scope, capi_name table, capi_name payer, uint64_t id, const long double* secondary); -extern "C" __attribute__((sysio_wasm_import)) void db_idx_long_double_update(int32_t iterator, capi_name payer, const long double* secondary); -extern "C" __attribute__((sysio_wasm_import)) void db_idx_long_double_remove(int32_t iterator); +// KV write intrinsics (must be rejected in read-only actions) +extern "C" __attribute__((sysio_wasm_import)) int64_t kv_set(uint32_t key_format, uint64_t payer, const void* key, uint32_t key_size, const void* value, uint32_t value_size); +extern "C" __attribute__((sysio_wasm_import)) int64_t kv_erase(uint32_t key_format, const void* key, uint32_t key_size); +extern "C" __attribute__((sysio_wasm_import)) void kv_idx_store(uint64_t payer, uint64_t table, uint32_t index_id, const void* pri_key, uint32_t pri_key_size, const void* sec_key, uint32_t sec_key_size); +extern "C" __attribute__((sysio_wasm_import)) void kv_idx_remove(uint64_t table, uint32_t index_id, const void* pri_key, uint32_t pri_key_size, const void* sec_key, uint32_t sec_key_size); +extern "C" __attribute__((sysio_wasm_import)) void kv_idx_update(uint64_t payer, uint64_t table, uint32_t index_id, const void* pri_key, uint32_t pri_key_size, const void* old_sec_key, uint32_t old_sec_key_size, const void* new_sec_key, uint32_t new_sec_key_size); extern "C" __attribute__((sysio_wasm_import)) int64_t set_proposed_producers( char*, uint32_t ); extern "C" __attribute__((sysio_wasm_import)) int64_t set_proposed_producers_ex( uint64_t producer_data_format, char *producer_data, uint32_t producer_data_size ); @@ -101,116 +88,30 @@ class [[sysio::contract]] host_functions_tests : public sysio::contract { set_privileged("sysio"_n, ispr); return true; } -/* all tested -db_store_i64 -db_update_i64 -db_remove_i64 -db_idx64_store -db_idx64_update -db_idx64_remove -db_idx128_store -db_idx128_update -db_idx128_remove -db_idx256_store -db_idx256_update -db_idx256_remove -db_idx_double_store -db_idx_double_update -db_idx_double_remove -db_idx_long_double_store -db_idx_long_double_update -db_idx_long_double_remove -*/ -// abcde means 67890 a4 means 64 12c means 128 so as to no conflict with naming rule -// Name should be less than 13 characters and only contains the following symbol 12345abcdefghijklmnopqrstuvwxyz - ACTION_TYPE - bool dbia4s(){ - db_store_i64(0, 0, 0, 0, NULL, 0); - return true; - } - ACTION_TYPE - bool dbia4u(){ - db_update_i64(0, 0, NULL, 0); - return true; - } - ACTION_TYPE - bool dbia4r(){ - db_remove_i64(0); - return true; - } - ACTION_TYPE - bool dbidxa4s() { - db_idx64_store(0, 0, 0, 0, NULL); - return true; - } - ACTION_TYPE - bool dbidxa4u() { - db_idx64_update(0, 0, NULL); - return true; - } - ACTION_TYPE - bool dbidxa4r() { - db_idx64_remove(0); - return true; - } - ACTION_TYPE - bool dbidx12cs() { - db_idx128_store(0, 0, 0, 0, NULL); - return true; - } - ACTION_TYPE - bool dbidx12cu() { - db_idx128_update(0, 0, NULL); - return true; - } - ACTION_TYPE - bool dbidx12cr() { - db_idx128_remove(0); - return true; - } - ACTION_TYPE - bool dbidx25as() { - db_idx256_store(0, 0, 0, 0, NULL, 0); - return true; - } - ACTION_TYPE - bool dbidx25au() { - db_idx256_update(0, 0, NULL, 0); - return true; - } - ACTION_TYPE - bool dbidx25ar() { - db_idx256_remove(0); - return true; - } - ACTION_TYPE - bool dbidxdbs(){ - db_idx_double_store(0, 0, 0, 0, NULL); - return true; - } +// KV write operations must be rejected in read-only actions ACTION_TYPE - bool dbidxdbu(){ - db_idx_double_update(0, 0, NULL); + bool kvset(){ + kv_set(0, 0, "k", 1, "v", 1); return true; } ACTION_TYPE - bool dbidxdbr(){ - db_idx_double_remove(0); + bool kverase(){ + kv_erase(0, "k", 1); return true; } ACTION_TYPE - bool dbidxldbs (){ - db_idx_long_double_store(0, 0, 0, 0, NULL); + bool kvidxstore(){ + kv_idx_store(0, 0, 0, "p", 1, "s", 1); return true; } ACTION_TYPE - bool dbidxldbu(){ - db_idx_long_double_update(0, 0, NULL); + bool kvidxremove(){ + kv_idx_remove(0, 0, "p", 1, "s", 1); return true; } ACTION_TYPE - bool dbidxldbr(){ - db_idx_long_double_remove(0); + bool kvidxupdate(){ + kv_idx_update(0, 0, 0, "p", 1, "s", 1, "t", 1); return true; } ACTION_TYPE diff --git a/tests/toolchain/compile-fail/host_functions_tests.json b/tests/toolchain/compile-fail/host_functions_tests.json index 1d02388a6..886060de4 100644 --- a/tests/toolchain/compile-fail/host_functions_tests.json +++ b/tests/toolchain/compile-fail/host_functions_tests.json @@ -4,7 +4,7 @@ "compile_flags": [], "expected" : { "exit-code": 255, - "stderr": "(codegen error.*){20}" + "stderr": "(codegen error.*){15}" } } ] diff --git a/tests/unit/test_contracts/CMakeLists.txt b/tests/unit/test_contracts/CMakeLists.txt index 1c40aa95b..359770727 100644 --- a/tests/unit/test_contracts/CMakeLists.txt +++ b/tests/unit/test_contracts/CMakeLists.txt @@ -13,6 +13,9 @@ 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(kv_table_tests kv_table_tests kv_table_tests.cpp) +add_contract(kv_raw_table_tests kv_raw_table_tests kv_raw_table_tests.cpp) +add_contract(kv_singleton_tests kv_singleton_tests kv_singleton_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/array_tests.cpp b/tests/unit/test_contracts/array_tests.cpp index 493ca9577..118b00e50 100644 --- a/tests/unit/test_contracts/array_tests.cpp +++ b/tests/unit/test_contracts/array_tests.cpp @@ -1,6 +1,7 @@ #include #include #include +#include using namespace sysio; @@ -19,7 +20,7 @@ class [[sysio::contract]] array_tests : public contract { std::string name; }; - typedef multi_index tests_table; + typedef kv_multi_index tests_table; typedef std::array array_string_4; struct my_struct { uint32_t id; diff --git a/tests/unit/test_contracts/capi/db.c b/tests/unit/test_contracts/capi/db.c index b57c82149..6bb98242b 100644 --- a/tests/unit/test_contracts/capi/db.c +++ b/tests/unit/test_contracts/capi/db.c @@ -1,70 +1,46 @@ -#include +// Legacy db_*_i64 C API tests replaced with KV intrinsic tests. +// The KV C API is declared in . + +#include #include void test_db( void ) { - db_store_i64(0, 0, 0, 0, NULL, 0); - db_update_i64(0, 0, NULL, 0); - db_remove_i64(0); - db_get_i64(0, NULL, 0); - db_next_i64(0, NULL); - db_previous_i64(0, NULL); - db_find_i64(0, 0, 0, 0); - db_lowerbound_i64(0, 0, 0, 0); - db_upperbound_i64(0, 0, 0, 0); - db_end_i64(0, 0, 0); - - db_idx64_store(0, 0, 0, 0, NULL); - db_idx64_update(0, 0, NULL); - db_idx64_remove(0); - db_idx64_next(0, NULL); - db_idx64_previous(0, NULL); - db_idx64_find_primary(0, 0, 0, NULL, 0); - db_idx64_find_secondary(0, 0, 0, NULL, NULL); - db_idx64_lowerbound(0, 0, 0, NULL, NULL); - db_idx64_upperbound(0, 0, 0, NULL, NULL); - db_idx64_end(0, 0, 0); - - db_idx128_store(0, 0, 0, 0, NULL); - db_idx128_update(0, 0, NULL); - db_idx128_remove(0); - db_idx128_next(0, NULL); - db_idx128_previous(0, NULL); - db_idx128_find_primary(0, 0, 0, NULL, 0); - db_idx128_find_secondary(0, 0, 0, NULL, NULL); - db_idx128_lowerbound(0, 0, 0, NULL, NULL); - db_idx128_upperbound(0, 0, 0, NULL, NULL); - db_idx128_end(0, 0, 0); + // Primary KV operations (format=0 raw) + kv_set(0, 0, "k", 1, "v", 1); + kv_get(0, 0, "k", 1, NULL, 0); + kv_erase(0, "k", 1); + kv_contains(0, 0, "k", 1); - db_idx256_store(0, 0, 0, 0, NULL, 0); - db_idx256_update(0, 0, NULL, 0); - db_idx256_remove(0); - db_idx256_next(0, NULL); - db_idx256_previous(0, NULL); - db_idx256_find_primary(0, 0, 0, NULL, 0, 0); - db_idx256_find_secondary(0, 0, 0, NULL,0, NULL); - db_idx256_lowerbound(0, 0, 0, NULL, 0, NULL); - db_idx256_upperbound(0, 0, 0, NULL, 0, NULL); - db_idx256_end(0, 0, 0); + // Primary KV operations (format=1 standard 24-byte key) + { + char key24[24] = {0}; + kv_set(1, 0, key24, 24, "val", 3); + kv_get(1, 0, key24, 24, NULL, 0); + kv_contains(1, 0, key24, 24); + kv_erase(1, key24, 24); + } - db_idx_double_store(0, 0, 0, 0, NULL); - db_idx_double_update(0, 0, NULL); - db_idx_double_remove(0); - db_idx_double_next(0, NULL); - db_idx_double_previous(0, NULL); - db_idx_double_find_primary(0, 0, 0, NULL, 0); - db_idx_double_find_secondary(0, 0, 0, NULL, NULL); - db_idx_double_lowerbound(0, 0, 0, NULL, NULL); - db_idx_double_upperbound(0, 0, 0, NULL, NULL); - db_idx_double_end(0, 0, 0); + // Primary iterators + uint32_t h = kv_it_create(0, 0, "p", 1); + kv_it_status(h); + kv_it_next(h); + kv_it_prev(h); + kv_it_lower_bound(h, "k", 1); + uint32_t actual = 0; + kv_it_key(h, 0, NULL, 0, &actual); + kv_it_value(h, 0, NULL, 0, &actual); + kv_it_destroy(h); - db_idx_long_double_store(0, 0, 0, 0, NULL); - db_idx_long_double_update(0, 0, NULL); - db_idx_long_double_remove(0); - db_idx_long_double_next(0, NULL); - db_idx_long_double_previous(0, NULL); - db_idx_long_double_find_primary(0, 0, 0, NULL, 0); - db_idx_long_double_find_secondary(0, 0, 0, NULL, NULL); - db_idx_long_double_lowerbound(0, 0, 0, NULL, NULL); - db_idx_long_double_upperbound(0, 0, 0, NULL, NULL); - db_idx_long_double_end(0, 0, 0); + // Secondary index operations + kv_idx_store(0, 0, 0, "p", 1, "s", 1); + kv_idx_update(0, 0, 0, "p", 1, "s", 1, "t", 1); + kv_idx_remove(0, 0, "p", 1, "s", 1); + int32_t sh = kv_idx_find_secondary(0, 0, 0, "s", 1); + int32_t lb = kv_idx_lower_bound(0, 0, 0, "s", 1); + (void)lb; + kv_idx_next(sh); + kv_idx_prev(sh); + kv_idx_key(sh, 0, NULL, 0, &actual); + kv_idx_primary_key(sh, 0, NULL, 0, &actual); + kv_idx_destroy(sh); } diff --git a/tests/unit/test_contracts/explicit_nested_tests.cpp b/tests/unit/test_contracts/explicit_nested_tests.cpp index 9e309fb0f..cbeda2e01 100644 --- a/tests/unit/test_contracts/explicit_nested_tests.cpp +++ b/tests/unit/test_contracts/explicit_nested_tests.cpp @@ -1,4 +1,5 @@ #include +#include using namespace std; @@ -36,7 +37,7 @@ CONTRACT explicit_nested_tests : public contract { SYSLIB_SERIALIZE(testdata, (id)(data)(data_vec)(data_vec2)(data_vec3)(tup1)(var1)(vvmys)); }; - using test_data_idx = multi_index<"testdata"_n, testdata>; + using test_data_idx = kv_multi_index<"testdata"_n, testdata>; [[sysio::action]] //usage: clio -v push action sysio vvstr '[[["abc", "cde"],["def","fgh"]]]' -p sysio@active diff --git a/tests/unit/test_contracts/get_code_hash_read.cpp b/tests/unit/test_contracts/get_code_hash_read.cpp index 4f0ffa2f1..2c80de978 100644 --- a/tests/unit/test_contracts/get_code_hash_read.cpp +++ b/tests/unit/test_contracts/get_code_hash_read.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "get_code_hash_table.hpp" @@ -8,7 +9,7 @@ class [[sysio::contract]] get_code_hash_tests : public contract { public: using contract::contract; - using hash_table = multi_index; + using hash_table = kv_multi_index; // Read the old code's hash from database and verify new code's hash differs [[sysio::action]] diff --git a/tests/unit/test_contracts/get_code_hash_write.cpp b/tests/unit/test_contracts/get_code_hash_write.cpp index 82e314e16..3c36b5130 100644 --- a/tests/unit/test_contracts/get_code_hash_write.cpp +++ b/tests/unit/test_contracts/get_code_hash_write.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "get_code_hash_table.hpp" @@ -8,7 +9,7 @@ class [[sysio::contract]] get_code_hash_tests : public contract { public: using contract::contract; - using hash_table = multi_index; + using hash_table = kv_multi_index; // Write this code's hash to database [[sysio::action]] diff --git a/tests/unit/test_contracts/kv_raw_table_tests.cpp b/tests/unit/test_contracts/kv_raw_table_tests.cpp new file mode 100644 index 000000000..c616c7977 --- /dev/null +++ b/tests/unit/test_contracts/kv_raw_table_tests.cpp @@ -0,0 +1,524 @@ +#include +#include + +using namespace sysio; + +class [[sysio::contract("kv_raw_table_tests")]] kv_raw_table_tests : public contract { +public: + using contract::contract; + + // ── Key/value types ────────────────────────────────────────────────────── + + struct uint_key { + uint64_t k; + SYSLIB_SERIALIZE(uint_key, (k)) + }; + struct uint_val { + uint64_t v; + SYSLIB_SERIALIZE(uint_val, (v)) + }; + + kv::raw_table uint_store; + + // ── Basic set/get/erase/contains ───────────────────────────────────────── + + [[sysio::action]] + void setget() { + uint_store.set({42}, {100}); + auto val = uint_store.get({42}); + check(val.has_value(), "get(42) should return value"); + check(val->v == 100, "value should be 100"); + + // Non-existent + auto no = uint_store.get({999}); + check(!no.has_value(), "get(999) should be empty"); + } + + [[sysio::action]] + void erasetest() { + uint_store.set({1}, {10}); + check(uint_store.contains({1}), "should contain key 1"); + + uint_store.erase({1}); + check(!uint_store.contains({1}), "should not contain key 1 after erase"); + + auto val = uint_store.get({1}); + check(!val.has_value(), "get should be empty after erase"); + } + + // ── Iteration ordering (unsigned) ──────────────────────────────────────── + + [[sysio::action]] + void uintorder() { + uint_store.set({30}, {3}); + uint_store.set({10}, {1}); + uint_store.set({50}, {5}); + uint_store.set({20}, {2}); + uint_store.set({40}, {4}); + + // Iteration should be in key order: 10, 20, 30, 40, 50 + uint64_t expected[] = {1, 2, 3, 4, 5}; + int idx = 0; + for (auto it = uint_store.begin(); it != uint_store.end(); ++it) { + check(idx < 5, "too many entries"); + check((*it).v == expected[idx], "wrong order in uint iteration"); + ++idx; + } + check(idx == 5, "should have 5 entries"); + } + + // ── Signed int64_t key ordering ────────────────────────────────────────── + // be_key_stream must apply sign-bit flip so negatives sort before positives. + + struct i64_key { + int64_t k; + SYSLIB_SERIALIZE(i64_key, (k)) + }; + struct i64_val { + int64_t original; + SYSLIB_SERIALIZE(i64_val, (original)) + }; + + kv::raw_table i64_store; + + [[sysio::action]] + void signedorder() { + int64_t keys[] = {100, -50, 0, -100, 50, -1, 1}; + for (auto k : keys) { + i64_store.set({k}, {k}); + } + + // Expected order: -100, -50, -1, 0, 1, 50, 100 + int64_t expected[] = {-100, -50, -1, 0, 1, 50, 100}; + int idx = 0; + for (auto it = i64_store.begin(); it != i64_store.end(); ++it) { + check(idx < 7, "too many entries"); + check((*it).original == expected[idx], "wrong signed key order"); + ++idx; + } + check(idx == 7, "should have 7 entries"); + } + + // ── Signed int32_t key ordering ────────────────────────────────────────── + + struct i32_key { + int32_t k; + SYSLIB_SERIALIZE(i32_key, (k)) + }; + struct i32_val { + int32_t original; + SYSLIB_SERIALIZE(i32_val, (original)) + }; + + kv::raw_table i32_store; + + [[sysio::action]] + void signed32() { + int32_t keys[] = {100, -50, 0, -100, 50}; + for (auto k : keys) { + i32_store.set({k}, {k}); + } + + int32_t expected[] = {-100, -50, 0, 50, 100}; + int idx = 0; + for (auto it = i32_store.begin(); it != i32_store.end(); ++it) { + check(idx < 5, "too many entries"); + check((*it).original == expected[idx], "wrong int32 key order"); + ++idx; + } + check(idx == 5, "should have 5 entries"); + } + + // ── Signed int16_t key ordering ────────────────────────────────────────── + + struct i16_key { + int16_t k; + SYSLIB_SERIALIZE(i16_key, (k)) + }; + struct i16_val { + int16_t original; + SYSLIB_SERIALIZE(i16_val, (original)) + }; + + kv::raw_table i16_store; + + [[sysio::action]] + void signedsm() { + int16_t keys[] = {100, -50, 0, -100, 50}; + for (auto k : keys) { + i16_store.set({k}, {k}); + } + + int16_t expected[] = {-100, -50, 0, 50, 100}; + int idx = 0; + for (auto it = i16_store.begin(); it != i16_store.end(); ++it) { + check(idx < 5, "too many entries"); + check((*it).original == expected[idx], "wrong int16 key order"); + ++idx; + } + check(idx == 5, "should have 5 entries"); + } + + // ── String key ordering ────────────────────────────────────────────────── + + struct str_key { + std::string region; + uint64_t id; + SYSLIB_SERIALIZE(str_key, (region)(id)) + }; + struct str_val { + std::string payload; + SYSLIB_SERIALIZE(str_val, (payload)) + }; + + kv::raw_table str_store; + + [[sysio::action]] + void strorder() { + str_store.set({"eu-west", 2}, {"ew2"}); + str_store.set({"us-east", 1}, {"ue1"}); + str_store.set({"eu-west", 1}, {"ew1"}); + str_store.set({"us-east", 2}, {"ue2"}); + + // Expected order: eu-west/1, eu-west/2, us-east/1, us-east/2 + std::string expected[] = {"ew1", "ew2", "ue1", "ue2"}; + int idx = 0; + for (auto it = str_store.begin(); it != str_store.end(); ++it) { + check(idx < 4, "too many entries"); + check((*it).payload == expected[idx], "wrong string key order"); + ++idx; + } + check(idx == 4, "should have 4 entries"); + } + + // ── lower_bound / upper_bound ──────────────────────────────────────────── + + [[sysio::action]] + void bounds() { + uint_store.set({10}, {1}); + uint_store.set({20}, {2}); + uint_store.set({30}, {3}); + + // lower_bound(15) should find key=20 + auto lb = uint_store.lower_bound({15}); + check(lb != uint_store.end(), "lower_bound(15) should not be end"); + check((*lb).v == 2, "lower_bound(15) value should be 2"); + + // lower_bound(20) should find key=20 (exact) + lb = uint_store.lower_bound({20}); + check(lb != uint_store.end() && (*lb).v == 2, "lower_bound(20) exact match"); + + // upper_bound(20) should find key=30 + auto ub = uint_store.upper_bound({20}); + check(ub != uint_store.end() && (*ub).v == 3, "upper_bound(20) should be 3"); + + // upper_bound(30) should be end + ub = uint_store.upper_bound({30}); + check(ub == uint_store.end(), "upper_bound(30) should be end"); + } + + // ── Empty map iteration ────────────────────────────────────────────────── + + [[sysio::action]] + void emptyiter() { + kv::raw_table empty_store; + check(empty_store.begin() == empty_store.end(), "empty map: begin==end"); + } + + // ── Overwrite existing key ─────────────────────────────────────────────── + + [[sysio::action]] + void overwrite() { + uint_store.set({1}, {100}); + uint_store.set({1}, {200}); + + auto val = uint_store.get({1}); + check(val.has_value() && val->v == 200, "overwrite should update value"); + } + + // ── Reverse iteration ──────────────────────────────────────────────────── + + [[sysio::action]] + void reviter() { + uint_store.set({10}, {1}); + uint_store.set({20}, {2}); + uint_store.set({30}, {3}); + + // Backward: should visit 30, 20, 10 + uint64_t expected[] = {3, 2, 1}; + int idx = 0; + auto it = uint_store.end(); + while (it != uint_store.begin()) { + --it; + check(idx < 3, "too many entries in reverse"); + check((*it).v == expected[idx], "wrong reverse order"); + ++idx; + } + check(idx == 3, "should have 3 entries in reverse"); + } + + // ── Float key ordering ────────────────────────────────────────────────── + + struct flt_key { + float k; + SYSLIB_SERIALIZE(flt_key, (k)) + }; + struct flt_val { + float original; + SYSLIB_SERIALIZE(flt_val, (original)) + }; + + kv::raw_table flt_store; + + [[sysio::action]] + void floatorder() { + float keys[] = {1.5f, -0.5f, 0.0f, -1.5f, 0.5f, -0.0f}; + for (auto k : keys) { + flt_store.set({k}, {k}); + } + + // -0.0 (0x80000000) and +0.0 (0x00000000) are different bit patterns, + // so they are different keys. After sign-magnitude flip: + // -0.0 -> ~0x80000000 = 0x7FFFFFFF, +0.0 -> 0x80000000 + // So -0.0 sorts just before +0.0. + // Expected order: -1.5, -0.5, -0.0, +0.0, 0.5, 1.5 + float expected[] = {-1.5f, -0.5f, -0.0f, 0.0f, 0.5f, 1.5f}; + int idx = 0; + for (auto it = flt_store.begin(); it != flt_store.end(); ++it) { + check(idx < 6, "too many float entries"); + // Use memcmp to distinguish -0.0 from +0.0 (they compare equal with ==) + uint32_t actual_bits, expected_bits; + float actual = (*it).original; + __builtin_memcpy(&actual_bits, &actual, 4); + __builtin_memcpy(&expected_bits, &expected[idx], 4); + check(actual_bits == expected_bits, "wrong float key order"); + ++idx; + } + check(idx == 6, "should have 6 float entries"); + } + + // ── Double key ordering ───────────────────────────────────────────────── + + struct dbl_key { + double k; + SYSLIB_SERIALIZE(dbl_key, (k)) + }; + struct dbl_val { + double original; + SYSLIB_SERIALIZE(dbl_val, (original)) + }; + + kv::raw_table dbl_store; + + [[sysio::action]] + void dblorder() { + double keys[] = {1.5, -0.5, 0.0, -1.5, 0.5}; + for (auto k : keys) { + dbl_store.set({k}, {k}); + } + + double expected[] = {-1.5, -0.5, 0.0, 0.5, 1.5}; + int idx = 0; + for (auto it = dbl_store.begin(); it != dbl_store.end(); ++it) { + check(idx < 5, "too many double entries"); + check((*it).original == expected[idx], "wrong double key order"); + ++idx; + } + check(idx == 5, "should have 5 double entries"); + } + + // ── int128_t key ordering ─────────────────────────────────────────────── + + struct i128_key { + int128_t k; + SYSLIB_SERIALIZE(i128_key, (k)) + }; + struct i128_val { + int128_t original; + SYSLIB_SERIALIZE(i128_val, (original)) + }; + + kv::raw_table i128_store; + + [[sysio::action]] + void signedi() { + int128_t keys[] = {100, -50, 0, -100, 50}; + for (auto k : keys) { + i128_store.set({k}, {k}); + } + + int128_t expected[] = {-100, -50, 0, 50, 100}; + int idx = 0; + for (auto it = i128_store.begin(); it != i128_store.end(); ++it) { + check(idx < 5, "too many int128 entries"); + check((*it).original == expected[idx], "wrong int128 key order"); + ++idx; + } + check(idx == 5, "should have 5 int128 entries"); + } + + // ── String with embedded NUL ──────────────────────────────────────────── + // NUL-escape encoding must sort strings with embedded 0x00 correctly. + + struct nullstr_key { + std::string k; + SYSLIB_SERIALIZE(nullstr_key, (k)) + }; + struct nullstr_val { + std::string label; + SYSLIB_SERIALIZE(nullstr_val, (label)) + }; + + kv::raw_table nullstr_store; + + [[sysio::action]] + void strnul() { + // "a\0b" (3 bytes) should sort after "a" (1 byte) and before "b" (1 byte) + std::string a_nul_b({'a', '\0', 'b'}); + std::string a("a"); + std::string b("b"); + + nullstr_store.set({b}, {"third"}); + nullstr_store.set({a_nul_b}, {"second"}); + nullstr_store.set({a}, {"first"}); + + std::string expected[] = {"first", "second", "third"}; + int idx = 0; + for (auto it = nullstr_store.begin(); it != nullstr_store.end(); ++it) { + check(idx < 3, "too many nullstr entries"); + check((*it).label == expected[idx], "wrong nullstr sort order"); + ++idx; + } + check(idx == 3, "should have 3 nullstr entries"); + } + + // ── vector key ──────────────────────────────────────────────────── + + struct blob_key { + std::vector k; + SYSLIB_SERIALIZE(blob_key, (k)) + }; + struct blob_val { + uint32_t label; + SYSLIB_SERIALIZE(blob_val, (label)) + }; + + kv::raw_table blob_store; + + [[sysio::action]] + void blobkey() { + std::vector k1 = {'a', '\0'}; // "a\0" + std::vector k2 = {'a', '\0', '\x01'}; // "a\0\x01" + std::vector k3 = {'a', '\x01'}; // "a\x01" + + blob_store.set({k3}, {3}); + blob_store.set({k1}, {1}); + blob_store.set({k2}, {2}); + + // Expected: "a\0" < "a\0\x01" < "a\x01" + uint32_t expected[] = {1, 2, 3}; + int idx = 0; + for (auto it = blob_store.begin(); it != blob_store.end(); ++it) { + check(idx < 3, "too many blob entries"); + check((*it).label == expected[idx], "wrong blob key order"); + ++idx; + } + check(idx == 3, "should have 3 blob entries"); + } + + // ── Cross-contract read via code parameter (N3) ────────────────────────── + + [[sysio::action]] + void crossread() { + // Write data with default (self) table + uint_store.set({77}, {770}); + + // Read back with explicit code = get_self() + kv::raw_table reader(get_self()); + auto val = reader.get({77}); + check(val.has_value(), "crossread: should find key 77 via explicit code"); + check(val->v == 770, "crossread: value should be 770"); + check(reader.contains({77}), "crossread: contains should return true"); + + // Iterate with explicit code + int count = 0; + for (auto it = reader.begin(); it != reader.end(); ++it) { + ++count; + } + check(count >= 1, "crossread: should iterate at least 1 entry"); + } + + // ── Negative: erase non-existent key (T1) ──────────────────────────────── + + [[sysio::action]] + void erasebad() { + // Erasing a key that was never set should abort + uint_store.erase({99999}); + } + + // ── Edge: INT64_MIN, INT64_MAX, 0 ─────────────────────────────────────── + + [[sysio::action]] + void signededge() { + int64_t keys[] = { + std::numeric_limits::max(), + 0, + std::numeric_limits::min(), + 1, + -1 + }; + for (auto k : keys) { + i64_store.set({k}, {k}); + } + + // Expected: INT64_MIN, -1, 0, 1, INT64_MAX + int64_t expected[] = { + std::numeric_limits::min(), + -1, 0, 1, + std::numeric_limits::max() + }; + int idx = 0; + for (auto it = i64_store.begin(); it != i64_store.end(); ++it) { + check(idx < 5, "too many entries"); + check((*it).original == expected[idx], "wrong edge-case key order"); + ++idx; + } + check(idx == 5, "should have 5 entries"); + } + + // ── T5: Zero-length value ───────────────────────────────────────────────── + + struct tiny_val { + uint8_t dummy = 0; + SYSLIB_SERIALIZE(tiny_val, (dummy)) + }; + + kv::raw_table tinyval_store; + + [[sysio::action]] + void zeroval() { + // Test with minimal 1-byte value (empty structs can't use SYSLIB_SERIALIZE) + tinyval_store.set({1}, {0}); + check(tinyval_store.contains({1}), "zeroval: key should exist"); + auto val = tinyval_store.get({1}); + check(val.has_value(), "zeroval: get should return value"); + + tinyval_store.erase({1}); + check(!tinyval_store.contains({1}), "zeroval: key should not exist after erase"); + } + + // ── T5: RAM delta from set/erase ───────────────────────────────────────── + + [[sysio::action]] + void ramdelta() { + int64_t delta_set = uint_store.set({500}, {5000}); + check(delta_set > 0, "ramdelta: set should return positive delta for new key"); + + // Overwrite same key — delta may be 0 (same size) but should not be negative + int64_t delta_overwrite = uint_store.set({500}, {9999}); + check(delta_overwrite == 0, "ramdelta: overwrite same-size value should return 0 delta"); + + int64_t delta_erase = uint_store.erase({500}); + check(delta_erase < 0, "ramdelta: erase should return negative delta"); + } +}; diff --git a/tests/unit/test_contracts/kv_singleton_tests.cpp b/tests/unit/test_contracts/kv_singleton_tests.cpp new file mode 100644 index 000000000..98c908b9b --- /dev/null +++ b/tests/unit/test_contracts/kv_singleton_tests.cpp @@ -0,0 +1,125 @@ +#include +#include + +using namespace sysio; + +class [[sysio::contract("kv_singleton_tests")]] kv_singleton_tests : public contract { +public: + using contract::contract; + + struct config { + uint64_t max_supply; + std::string symbol; + SYSLIB_SERIALIZE(config, (max_supply)(symbol)) + }; + + using config_singleton = singleton<"config"_n, config>; + + // ── exists / set / get ─────────────────────────────────────────────────── + + [[sysio::action]] + void setget() { + config_singleton cfg(get_self(), get_self().value); + + check(!cfg.exists(), "should not exist initially"); + + cfg.set({1000000, "SYS"}, get_self()); + + check(cfg.exists(), "should exist after set"); + + auto val = cfg.get(); + check(val.max_supply == 1000000, "max_supply mismatch"); + check(val.symbol == "SYS", "symbol mismatch"); + } + + // ── get_or_default ─────────────────────────────────────────────────────── + + [[sysio::action]] + void getdefault() { + config_singleton cfg(get_self(), "default"_n.value); + + // Should return default when not set + auto val = cfg.get_or_default({500, "DEF"}); + check(val.max_supply == 500, "default max_supply mismatch"); + check(val.symbol == "DEF", "default symbol mismatch"); + + // After set, should return stored value + cfg.set({999, "SET"}, get_self()); + val = cfg.get_or_default({500, "DEF"}); + check(val.max_supply == 999, "stored max_supply mismatch"); + check(val.symbol == "SET", "stored symbol mismatch"); + } + + // ── get_or_create ──────────────────────────────────────────────────────── + + [[sysio::action]] + void getorcreate() { + config_singleton cfg(get_self(), "goc"_n.value); + + // First call: creates with default + auto val = cfg.get_or_create(get_self(), {777, "NEW"}); + check(val.max_supply == 777, "created max_supply mismatch"); + + // Second call: returns existing + val = cfg.get_or_create(get_self(), {888, "OTHER"}); + check(val.max_supply == 777, "should return existing, not new default"); + } + + // ── remove ─────────────────────────────────────────────────────────────── + + [[sysio::action]] + void removetest() { + config_singleton cfg(get_self(), "remove"_n.value); + + cfg.set({100, "RM"}, get_self()); + check(cfg.exists(), "should exist before remove"); + + cfg.remove(); + check(!cfg.exists(), "should not exist after remove"); + + // Double remove should be safe + cfg.remove(); + check(!cfg.exists(), "still should not exist"); + } + + // ── set overwrites existing value ──────────────────────────────────────── + + [[sysio::action]] + void settwice() { + config_singleton cfg(get_self(), "twice"_n.value); + + cfg.set({100, "FIRST"}, get_self()); + cfg.set({200, "SECOND"}, get_self()); + + auto val = cfg.get(); + check(val.max_supply == 200, "should be overwritten value"); + check(val.symbol == "SECOND", "should be overwritten symbol"); + } + + // ── Different scopes are independent ───────────────────────────────────── + + // Negative: get() on unset singleton should assert (T2) + [[sysio::action]] + void getunset() { + config_singleton cfg(get_self(), "empty"_n.value); + cfg.get(); // should abort: "singleton does not exist" + } + + [[sysio::action]] + void scopetest() { + config_singleton cfg1(get_self(), "scope1"_n.value); + config_singleton cfg2(get_self(), "scope2"_n.value); + + cfg1.set({111, "S1"}, get_self()); + cfg2.set({222, "S2"}, get_self()); + + check(cfg1.get().max_supply == 111, "scope1 value mismatch"); + check(cfg2.get().max_supply == 222, "scope2 value mismatch"); + + // Removing from one scope shouldn't affect the other + cfg1.remove(); + check(!cfg1.exists(), "scope1 should be removed"); + check(cfg2.exists(), "scope2 should still exist"); + check(cfg2.get().max_supply == 222, "scope2 value should be unchanged"); + } +}; diff --git a/tests/unit/test_contracts/kv_table_tests.cpp b/tests/unit/test_contracts/kv_table_tests.cpp new file mode 100644 index 000000000..047971cba --- /dev/null +++ b/tests/unit/test_contracts/kv_table_tests.cpp @@ -0,0 +1,331 @@ +#include +#include + +using namespace sysio; + +class [[sysio::contract("kv_table_tests")]] kv_table_tests : public contract { +public: + using contract::contract; + + // ── Row type ───────────────────────────────────────────────────────────── + + struct [[sysio::table]] tbl_row { + uint64_t id; + uint64_t value; + std::string label; + + uint64_t primary_key() const { return id; } + SYSLIB_SERIALIZE(tbl_row, (id)(value)(label)) + }; + + using test_table = kv::table<"testtbl"_n, tbl_row>; + + // ── Basic CRUD ─────────────────────────────────────────────────────────── + + [[sysio::action]] + void emplacefind() { + test_table t(get_self(), get_self().value); + + t.emplace(get_self(), [](tbl_row& r) { r.id = 1; r.value = 100; r.label = "one"; }); + t.emplace(get_self(), [](tbl_row& r) { r.id = 2; r.value = 200; r.label = "two"; }); + t.emplace(get_self(), [](tbl_row& r) { r.id = 3; r.value = 300; r.label = "three"; }); + + // find existing + auto itr = t.find(2); + check(itr != t.end(), "find(2) should succeed"); + check(itr->value == 200, "find(2) value mismatch"); + check(itr->label == "two", "find(2) label mismatch"); + + // find non-existing + check(t.find(99) == t.end(), "find(99) should be end"); + + // contains + check(t.contains(1), "contains(1) should be true"); + check(!t.contains(99), "contains(99) should be false"); + } + + [[sysio::action]] + void modify() { + test_table t(get_self(), "modify"_n.value); + + t.emplace(get_self(), [](tbl_row& r) { r.id = 1; r.value = 10; r.label = "orig"; }); + + auto itr = t.find(1); + check(itr != t.end(), "should find row"); + + t.modify(itr, get_self(), [](tbl_row& r) { + r.value = 42; + r.label = "modified"; + }); + + // Re-read and verify + auto itr2 = t.find(1); + check(itr2 != t.end(), "row should still exist"); + check(itr2->value == 42, "value should be 42"); + check(itr2->label == "modified", "label should be modified"); + } + + [[sysio::action]] + void erase() { + test_table t(get_self(), "erase"_n.value); + + t.emplace(get_self(), [](tbl_row& r) { r.id = 1; r.value = 10; r.label = "x"; }); + t.emplace(get_self(), [](tbl_row& r) { r.id = 2; r.value = 20; r.label = "y"; }); + + // Erase by object + auto itr = t.find(1); + check(itr != t.end(), "should find row 1"); + t.erase(itr); + check(t.find(1) == t.end(), "row 1 should be gone"); + + // Erase by pk + t.erase(2); + check(t.find(2) == t.end(), "row 2 should be gone"); + } + + // ── Iteration ──────────────────────────────────────────────────────────── + + [[sysio::action]] + void iterate() { + test_table t(get_self(), "iter"_n.value); + + t.emplace(get_self(), [](tbl_row& r) { r.id = 30; r.value = 3; r.label = "c"; }); + t.emplace(get_self(), [](tbl_row& r) { r.id = 10; r.value = 1; r.label = "a"; }); + t.emplace(get_self(), [](tbl_row& r) { r.id = 20; r.value = 2; r.label = "b"; }); + + // Forward iteration should be in primary key order + uint64_t expected[] = {10, 20, 30}; + int idx = 0; + for (auto it = t.begin(); it != t.end(); ++it) { + check(idx < 3, "too many entries"); + check(it->id == expected[idx], "wrong order in forward iteration"); + ++idx; + } + check(idx == 3, "should have 3 entries"); + } + + [[sysio::action]] + void lowerupper() { + test_table t(get_self(), "bounds"_n.value); + + t.emplace(get_self(), [](tbl_row& r) { r.id = 10; r.value = 1; r.label = ""; }); + t.emplace(get_self(), [](tbl_row& r) { r.id = 20; r.value = 2; r.label = ""; }); + t.emplace(get_self(), [](tbl_row& r) { r.id = 30; r.value = 3; r.label = ""; }); + + // lower_bound(15) should return pk=20 + auto lb = t.lower_bound(15); + check(lb != t.end() && lb->id == 20, "lower_bound(15) should be pk 20"); + + // lower_bound(20) should return pk=20 (exact match) + lb = t.lower_bound(20); + check(lb != t.end() && lb->id == 20, "lower_bound(20) should be pk 20"); + + // upper_bound(20) should return pk=30 + auto ub = t.upper_bound(20); + check(ub != t.end() && ub->id == 30, "upper_bound(20) should be pk 30"); + + // upper_bound(30) should be end + ub = t.upper_bound(30); + check(ub == t.end(), "upper_bound(30) should be end"); + } + + // ── get() returns by value (no static aliasing) ────────────────────────── + + [[sysio::action]] + void getbyval() { + test_table t(get_self(), "getval"_n.value); + + t.emplace(get_self(), [](tbl_row& r) { r.id = 1; r.value = 111; r.label = "first"; }); + t.emplace(get_self(), [](tbl_row& r) { r.id = 2; r.value = 222; r.label = "second"; }); + + // get() returns by value — two calls must not alias + auto a = t.get(1); + auto b = t.get(2); + check(a.value == 111, "a should still be 111"); + check(b.value == 222, "b should be 222"); + } + + // ── require_find ───────────────────────────────────────────────────────── + + [[sysio::action]] + void reqfind() { + test_table t(get_self(), "reqfind"_n.value); + + t.emplace(get_self(), [](tbl_row& r) { r.id = 42; r.value = 999; r.label = ""; }); + + auto itr = t.require_find(42); + check(itr != t.end() && itr->value == 999, "require_find should succeed"); + } + + [[sysio::action]] + void reqfindfail() { + test_table t(get_self(), "reqffail"_n.value); + // Should fail — table is empty + t.require_find(1, "expected failure"); + } + + // ── available_primary_key ──────────────────────────────────────────────── + + [[sysio::action]] + void availpk() { + test_table t(get_self(), "availpk"_n.value); + + // Empty table should return 0 + check(t.available_primary_key() == 0, "empty table should yield pk=0"); + + t.emplace(get_self(), [](tbl_row& r) { r.id = 5; r.value = 0; r.label = ""; }); + check(t.available_primary_key() == 6, "after pk=5, next should be 6"); + + t.emplace(get_self(), [](tbl_row& r) { r.id = 100; r.value = 0; r.label = ""; }); + check(t.available_primary_key() == 101, "after pk=100, next should be 101"); + } + + // ── Cross-scope iteration ──────────────────────────────────────────────── + + [[sysio::action]] + void crossscope() { + // Write rows into two different scopes + { + test_table t1(get_self(), "scope1"_n.value); + t1.emplace(get_self(), [](tbl_row& r) { r.id = 1; r.value = 10; r.label = "s1"; }); + } + { + test_table t2(get_self(), "scope2"_n.value); + t2.emplace(get_self(), [](tbl_row& r) { r.id = 2; r.value = 20; r.label = "s2"; }); + } + + // Cross-scope iteration should see both + test_table t(get_self(), 0); + uint32_t count = 0; + for (auto it = t.begin_all_scopes(); it != t.end_all_scopes(); ++it) { + ++count; + } + check(count == 2, "cross-scope should see exactly 2 rows"); + } + + // ── Empty table iteration ──────────────────────────────────────────────── + + [[sysio::action]] + void emptyiter() { + test_table t(get_self(), "empty"_n.value); + check(t.begin() == t.end(), "empty table: begin should equal end"); + } + + // ── cbegin / cend ───────────────────────────────────────────────────────── + + [[sysio::action]] + void constiter() { + test_table t(get_self(), "citer"_n.value); + + t.emplace(get_self(), [](tbl_row& r) { r.id = 1; r.value = 10; r.label = "a"; }); + t.emplace(get_self(), [](tbl_row& r) { r.id = 2; r.value = 20; r.label = "b"; }); + + // cbegin/cend should work identically to begin/end + auto cit = t.cbegin(); + check(cit != t.cend(), "cbegin should not equal cend"); + check(cit->id == 1, "cbegin should point to first row"); + ++cit; + check(cit->id == 2, "second via cbegin"); + ++cit; + check(cit == t.cend(), "should be cend after 2"); + } + + // ── erase by primary key ──────────────────────────────────────────────── + + [[sysio::action]] + void erasebypk() { + test_table t(get_self(), "erbypk"_n.value); + + t.emplace(get_self(), [](tbl_row& r) { r.id = 1; r.value = 10; r.label = "x"; }); + check(t.contains(1), "should exist before erase"); + + t.erase(1); + check(!t.contains(1), "should be gone after erase by pk"); + } + + // ── set() convenience method ───────────────────────────────────────────── + + [[sysio::action]] + void setmethod() { + test_table t(get_self(), "setm"_n.value); + + tbl_row r{1, 42, "direct"}; + t.set(r.id, r); + + auto itr = t.find(1); + check(itr != t.end(), "set row should exist"); + check(itr->value == 42, "set row value mismatch"); + check(itr->label == "direct", "set row label mismatch"); + + // Overwrite + tbl_row r2{1, 99, "updated"}; + t.set(r2.id, r2); + auto itr2 = t.find(1); + check(itr2 != t.end() && itr2->value == 99, "overwritten value mismatch"); + } + + // ── modify by object reference ─────────────────────────────────────────── + + [[sysio::action]] + void modifyobj() { + test_table t(get_self(), "modobj"_n.value); + + t.emplace(get_self(), [](tbl_row& r) { r.id = 1; r.value = 10; r.label = "orig"; }); + + auto obj = t.get(1); + t.modify(obj, get_self(), [](tbl_row& r) { + r.value = 77; + r.label = "by_obj"; + }); + + auto updated = t.get(1); + check(updated.value == 77, "modifyobj: value should be 77"); + check(updated.label == "by_obj", "modifyobj: label should be by_obj"); + } + + // ── end_all_scopes explicit ────────────────────────────────────────────── + + // A separate table type for the end_all_scopes test so it starts empty + struct [[sysio::table]] eas_row { + uint64_t id; + uint64_t primary_key() const { return id; } + SYSLIB_SERIALIZE(eas_row, (id)) + }; + using eas_table = kv::table<"eastbl"_n, eas_row>; + + // Negative: erase non-existent pk (T6) + [[sysio::action]] + void erasebadpk() { + test_table t(get_self(), get_self().value); + t.erase(99999); // should abort: key not found + } + + // Negative: modify that changes primary key (T6) + [[sysio::action]] + void modifypk() { + test_table t(get_self(), get_self().value); + t.emplace(get_self(), [](tbl_row& r) { r.id = 1; r.value = 100; r.label = "orig"; }); + auto itr = t.find(1); + t.modify(itr, get_self(), [](tbl_row& r) { r.id = 999; }); // should abort: cannot change pk + } + + [[sysio::action]] + void endallscope() { + eas_table t(get_self(), 0); + + // Empty table — begin_all_scopes should equal end_all_scopes + check(t.begin_all_scopes() == t.end_all_scopes(), + "endallscope: empty should be begin==end"); + + // Add data, then verify end is reachable + { + eas_table t1(get_self(), "eas"_n.value); + t1.emplace(get_self(), [](eas_row& r) { r.id = 1; }); + } + + uint32_t count = 0; + for (auto it = t.begin_all_scopes(); it != t.end_all_scopes(); ++it) + ++count; + check(count >= 1, "endallscope: should find at least 1 row"); + } +}; diff --git a/tests/unit/test_contracts/multi_index_tests.cpp b/tests/unit/test_contracts/multi_index_tests.cpp index c2b2f9d73..2c6050351 100644 --- a/tests/unit/test_contracts/multi_index_tests.cpp +++ b/tests/unit/test_contracts/multi_index_tests.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -78,7 +79,7 @@ namespace _test_multi_index size_t num_records = sizeof(records) / sizeof(records[0]); // Construct and fill table using multi_index - sysio::multi_index>> table(receiver, receiver.value); @@ -99,7 +100,7 @@ namespace _test_multi_index typedef record_idx64 record; // Load table using multi_index - sysio::multi_index>> table(receiver, receiver.value); @@ -210,7 +211,7 @@ namespace _test_multi_index typedef record_idx64 record; // Load table using multi_index - sysio::multi_index table(receiver, receiver.value); + sysio::kv_multi_index table(receiver, receiver.value); // make sure we're looking at the right table auto itr = table.require_find(781, "table not loaded"); @@ -227,7 +228,7 @@ namespace _test_multi_index typedef record_idx64 record; // Load table using multi_index - sysio::multi_index table(receiver, receiver.value); + sysio::kv_multi_index table(receiver, receiver.value); // make sure we're looking at the right table auto itr = table.require_find(234, "table not loaded"); @@ -244,7 +245,7 @@ namespace _test_multi_index typedef record_idx64 record; // Load table using multi_index - sysio::multi_index>> table(receiver, receiver.value); + sysio::kv_multi_index>> table(receiver, receiver.value); auto sec_index = table.template get_index<"bysecondary"_n>(); // make sure we're looking at the right table @@ -262,7 +263,7 @@ namespace _test_multi_index typedef record_idx64 record; // Load table using multi_index - sysio::multi_index>> table(receiver, receiver.value); + sysio::kv_multi_index>> table(receiver, receiver.value); auto sec_index = table.template get_index<"bysecondary"_n>(); // make sure we're looking at the right table @@ -280,7 +281,7 @@ namespace _test_multi_index typedef record_idx128 record; // Construct and fill table using multi_index - sysio::multi_index>> table(receiver, receiver.value); @@ -301,7 +302,7 @@ namespace _test_multi_index typedef record_idx128 record; // Load table using multi_index - sysio::multi_index>> table(receiver, receiver.value); @@ -335,7 +336,7 @@ namespace _test_multi_index { typedef record_idx64 record; // Load table using multi_index - sysio::multi_index>> table(receiver, receiver.value); return table; @@ -428,7 +429,7 @@ class [[sysio::contract]] test_multi_index : public sysio::contract sysio::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 + // KV implementation: no cross-table iterator check (dev safety only, no data impact) table2_sec_index.iterator_to(*table1_pk_itr); } @@ -562,7 +563,7 @@ class [[sysio::contract]] test_multi_index : public sysio::contract auto payer = get_self(); - sysio::multi_index<"autoinctbl1"_n, record, + sysio::kv_multi_index<"autoinctbl1"_n, record, sysio::indexed_by<"bysecondary"_n, sysio::const_mem_fun> > table( get_self(), get_self().value ); @@ -607,7 +608,7 @@ class [[sysio::contract]] test_multi_index : public sysio::contract auto payer = get_self(); - sysio::multi_index<"autoinctbl2"_n, record, + sysio::kv_multi_index<"autoinctbl2"_n, record, sysio::indexed_by<"bysecondary"_n, sysio::const_mem_fun> > table( get_self(), get_self().value ); @@ -638,14 +639,14 @@ class [[sysio::contract]] test_multi_index : public sysio::contract auto payer = get_self(); { - sysio::multi_index> > table( get_self(), get_self().value ); sysio::check( table.available_primary_key() == 3, "idx128_autoincrement_test_part2 - did not recover expected next primary key" ); } - sysio::multi_index> > table( get_self(), get_self().value ); @@ -691,7 +692,7 @@ class [[sysio::contract]] test_multi_index : public sysio::contract auto payer = get_self(); sysio::print("Testing checksum256 secondary index.\n"); - sysio::multi_index<"indextable5"_n, record, + sysio::kv_multi_index<"indextable5"_n, record, sysio::indexed_by<"bysecondary"_n, sysio::const_mem_fun> > table( get_self(), get_self().value ); @@ -795,7 +796,7 @@ class [[sysio::contract]] test_multi_index : public sysio::contract auto payer = get_self(); sysio::print("Testing double secondary index.\n"); - sysio::multi_index<"floattable1"_n, record, + sysio::kv_multi_index<"floattable1"_n, record, sysio::indexed_by<"bysecondary"_n, sysio::const_mem_fun> > table( get_self(), get_self().value ); @@ -839,6 +840,227 @@ class [[sysio::contract]] test_multi_index : public sysio::contract } } + // ── Secondary iterator clone with duplicate keys ─────────────────────── + // When multiple rows share the same secondary key, copying an iterator + // must preserve the exact position (matching primary key). + [[sysio::action("s1clone")]] void idx64_sec_clone_dup() { + using namespace _test_multi_index; + typedef record_idx64 record; + + sysio::kv_multi_index<"clonetbl"_n, record, + sysio::indexed_by<"bysecondary"_n, sysio::const_mem_fun> + > table(get_self(), get_self().value); + + auto payer = get_self(); + + // Three rows with the SAME secondary key + table.emplace(payer, [](auto& r) { r.id = 10; r.sec = 42; }); + table.emplace(payer, [](auto& r) { r.id = 20; r.sec = 42; }); + table.emplace(payer, [](auto& r) { r.id = 30; r.sec = 42; }); + + auto idx = table.get_index<"bysecondary"_n>(); + auto it = idx.begin(); + sysio::check(it != idx.end() && it->id == 10, "clone: first should be pk 10"); + + // Advance to second entry (pk=20) + ++it; + sysio::check(it->id == 20, "clone: second should be pk 20"); + + // Copy the iterator — must land on pk=20, not pk=10 + auto it_copy = it; + sysio::check(it_copy->id == 20, "clone: copy must preserve position at pk 20"); + + // Advance the copy — should go to pk=30 + ++it_copy; + sysio::check(it_copy->id == 30, "clone: copy++ should be pk 30"); + + // Original should still be at pk=20 + sysio::check(it->id == 20, "clone: original should still be pk 20"); + } + + // ── Secondary rbegin/rend ─────────────────────────────────────────────── + [[sysio::action("s1secrb")]] void idx64_sec_rbegin() { + using namespace _test_multi_index; + typedef record_idx64 record; + + sysio::kv_multi_index<"secrbtbl"_n, record, + sysio::indexed_by<"bysecondary"_n, sysio::const_mem_fun> + > table(get_self(), get_self().value); + + auto payer = get_self(); + table.emplace(payer, [](auto& r) { r.id = 1; r.sec = 50; }); + table.emplace(payer, [](auto& r) { r.id = 2; r.sec = 20; }); + table.emplace(payer, [](auto& r) { r.id = 3; r.sec = 40; }); + + auto idx = table.get_index<"bysecondary"_n>(); + + // rbegin should be highest secondary key (50, pk=1) + auto rit = idx.rbegin(); + sysio::check(rit != idx.rend(), "secrb: rbegin should not be rend"); + sysio::check(rit->sec == 50, "secrb: rbegin should be sec 50"); + ++rit; + sysio::check(rit->sec == 40, "secrb: second should be sec 40"); + ++rit; + sysio::check(rit->sec == 20, "secrb: third should be sec 20"); + ++rit; + sysio::check(rit == idx.rend(), "secrb: should be rend after 3"); + } + + // ── uint128_t secondary rbegin ────────────────────────────────────────── + // operator-- from end must use a buffer large enough for the key type. + [[sysio::action("s2secrb")]] void idx128_sec_rbegin() { + using namespace _test_multi_index; + typedef record_idx128 record; + + sysio::kv_multi_index<"s2rbtbl"_n, record, + sysio::indexed_by<"bysecondary"_n, sysio::const_mem_fun> + > table(get_self(), get_self().value); + + auto payer = get_self(); + // Use values > 2^64 to ensure uint128_t encoding matters + uint128_t lo = uint128_t(1) << 100; + uint128_t mid = uint128_t(1) << 110; + uint128_t hi = uint128_t(1) << 120; + table.emplace(payer, [&](auto& r) { r.id = 1; r.sec = lo; }); + table.emplace(payer, [&](auto& r) { r.id = 2; r.sec = mid; }); + table.emplace(payer, [&](auto& r) { r.id = 3; r.sec = hi; }); + + auto idx = table.get_index<"bysecondary"_n>(); + + auto rit = idx.rbegin(); + sysio::check(rit != idx.rend(), "s2rb: rbegin should not be rend"); + sysio::check(rit->id == 3, "s2rb: rbegin should be pk 3 (highest)"); + ++rit; + sysio::check(rit->id == 2, "s2rb: second should be pk 2"); + ++rit; + sysio::check(rit->id == 1, "s2rb: third should be pk 1"); + ++rit; + sysio::check(rit == idx.rend(), "s2rb: should be rend after 3"); + } + + // ── Name-typed primary key ────────────────────────────────────────────── + struct name_row { + sysio::name account; + uint64_t balance; + + sysio::name primary_key() const { return account; } + SYSLIB_SERIALIZE(name_row, (account)(balance)) + }; + + [[sysio::action("namepk")]] void name_primary_key() { + sysio::kv_multi_index<"namepktbl"_n, name_row> table(get_self(), get_self().value); + auto payer = get_self(); + + table.emplace(payer, [](auto& r) { r.account = "alice"_n; r.balance = 100; }); + table.emplace(payer, [](auto& r) { r.account = "bob"_n; r.balance = 200; }); + table.emplace(payer, [](auto& r) { r.account = "charlie"_n; r.balance = 300; }); + + // Find by name + auto itr = table.find("bob"_n); + sysio::check(itr != table.end(), "namepk: find(bob) should succeed"); + sysio::check(itr->balance == 200, "namepk: bob balance mismatch"); + + // Iterator copy (exercises to_pk_uint64 fix) + auto itr_copy = itr; + sysio::check(itr_copy->account == "bob"_n, "namepk: copy should be bob"); + + // Modify + table.modify(itr, payer, [](auto& r) { r.balance = 999; }); + auto itr2 = table.find("bob"_n); + sysio::check(itr2->balance == 999, "namepk: modified balance mismatch"); + + // Erase + table.erase(*itr2); + sysio::check(table.find("bob"_n) == table.end(), "namepk: bob should be gone"); + } + + // ── cbegin / cend ──────────────────────────────────────────────────────── + [[sysio::action("cbegincend")]] void cbegin_cend_test() { + using namespace _test_multi_index; + typedef record_idx64 record; + + sysio::kv_multi_index<"cbctbl"_n, record, + sysio::indexed_by<"bysecondary"_n, sysio::const_mem_fun> + > table(get_self(), get_self().value); + + auto payer = get_self(); + table.emplace(payer, [](auto& r) { r.id = 1; r.sec = 10; }); + table.emplace(payer, [](auto& r) { r.id = 2; r.sec = 20; }); + + auto cit = table.cbegin(); + sysio::check(cit != table.cend(), "cbegin: should not be cend"); + sysio::check(cit->id == 1, "cbegin: first should be pk 1"); + ++cit; + sysio::check(cit->id == 2, "cbegin: second should be pk 2"); + ++cit; + sysio::check(cit == table.cend(), "cbegin: should be cend after 2"); + + // Secondary cbegin/cend + auto idx = table.get_index<"bysecondary"_n>(); + auto scit = idx.cbegin(); + sysio::check(scit != idx.cend(), "sec cbegin: should not be cend"); + sysio::check(scit->sec == 10, "sec cbegin: first should be sec 10"); + ++scit; + sysio::check(scit->sec == 20, "sec cbegin: second should be sec 20"); + ++scit; + sysio::check(scit == idx.cend(), "sec cbegin: should be cend after 2"); + } + + // ── get_code / get_scope ────────────────────────────────────────────── + [[sysio::action("codescope")]] void code_scope_test() { + using namespace _test_multi_index; + typedef record_idx64 record; + + sysio::kv_multi_index<"codescopetbl"_n, record, + sysio::indexed_by<"bysecondary"_n, sysio::const_mem_fun> + > table(get_self(), "myscope"_n.value); + + sysio::check(table.get_code() == get_self(), "codescope: get_code should be self"); + sysio::check(table.get_scope() == "myscope"_n.value, "codescope: get_scope should be myscope"); + + auto idx = table.get_index<"bysecondary"_n>(); + sysio::check(idx.get_code() == get_self(), "codescope: sec get_code should be self"); + sysio::check(idx.get_scope() == "myscope"_n.value, "codescope: sec get_scope should be myscope"); + } + + // ── crbegin / crend (const reverse iterators) ─────────────────────────── + [[sysio::action("crbeginend")]] void const_reverse_iter() { + using namespace _test_multi_index; + typedef record_idx64 record; + + sysio::kv_multi_index<"crbtbl"_n, record, + sysio::indexed_by<"bysecondary"_n, sysio::const_mem_fun> + > table(get_self(), get_self().value); + + auto payer = get_self(); + table.emplace(payer, [](auto& r) { r.id = 10; r.sec = 1; }); + table.emplace(payer, [](auto& r) { r.id = 20; r.sec = 2; }); + table.emplace(payer, [](auto& r) { r.id = 30; r.sec = 3; }); + + // Primary crbegin/crend + auto rit = table.crbegin(); + sysio::check(rit != table.crend(), "crbegin: should not be crend"); + sysio::check(rit->id == 30, "crbegin: first should be pk 30"); + ++rit; + sysio::check(rit->id == 20, "crbegin: second should be pk 20"); + ++rit; + sysio::check(rit->id == 10, "crbegin: third should be pk 10"); + ++rit; + sysio::check(rit == table.crend(), "crbegin: should be crend after 3"); + + // Secondary crbegin/crend + auto idx = table.get_index<"bysecondary"_n>(); + auto srit = idx.crbegin(); + sysio::check(srit != idx.crend(), "sec crbegin: should not be crend"); + sysio::check(srit->sec == 3, "sec crbegin: first should be sec 3"); + ++srit; + sysio::check(srit->sec == 2, "sec crbegin: second should be sec 2"); + ++srit; + sysio::check(srit->sec == 1, "sec crbegin: third should be sec 1"); + ++srit; + sysio::check(srit == idx.crend(), "sec crbegin: should be crend after 3"); + } + [[sysio::action("sldg")]] void idx_long_double_general() { using namespace _test_multi_index; @@ -847,7 +1069,7 @@ class [[sysio::contract]] test_multi_index : public sysio::contract auto payer = get_self(); sysio::print("Testing long double secondary index.\n"); - sysio::multi_index<"floattable2"_n, record, + sysio::kv_multi_index<"floattable2"_n, record, sysio::indexed_by<"bysecondary"_n, sysio::const_mem_fun> > table( get_self(), get_self().value ); @@ -892,4 +1114,57 @@ class [[sysio::contract]] test_multi_index : public sysio::contract } } + + // T3: Verify kv_idx_update — modify secondary key, then verify secondary index reflects the change + [[sysio::action("s1secupd")]] void idx64_secondary_update() { + using namespace _test_multi_index; + typedef record_idx64 record; + auto payer = get_self(); + + sysio::kv_multi_index<"secupd"_n, record, + sysio::indexed_by<"bysecondary"_n, sysio::const_mem_fun> + > table(payer, payer.value); + + // Insert 3 records: alice(10), bob(20), charlie(30) + table.emplace(payer, [&](auto& r) { r.id = 1; r.sec = "alice"_n.value; }); + table.emplace(payer, [&](auto& r) { r.id = 2; r.sec = "bob"_n.value; }); + table.emplace(payer, [&](auto& r) { r.id = 3; r.sec = "charlie"_n.value; }); + + auto sec = table.get_index<"bysecondary"_n>(); + + // Verify initial secondary order: alice, bob, charlie + { + auto it = sec.begin(); + sysio::check(it->sec == "alice"_n.value, "s1secupd - initial order[0]"); + ++it; + sysio::check(it->sec == "bob"_n.value, "s1secupd - initial order[1]"); + ++it; + sysio::check(it->sec == "charlie"_n.value,"s1secupd - initial order[2]"); + } + + // Modify bob -> zoe (should move from middle to end in secondary order) + auto pk_itr = table.find(2); + sysio::check(pk_itr != table.end(), "s1secupd - find bob"); + table.modify(pk_itr, payer, [&](auto& r) { r.sec = "zoe"_n.value; }); + + // Verify new secondary order: alice, charlie, zoe + { + auto it = sec.begin(); + sysio::check(it->sec == "alice"_n.value, "s1secupd - after order[0]"); + ++it; + sysio::check(it->sec == "charlie"_n.value, "s1secupd - after order[1]"); + ++it; + sysio::check(it->sec == "zoe"_n.value, "s1secupd - after order[2]"); + ++it; + sysio::check(it == sec.end(), "s1secupd - after order end"); + } + + // Verify old key no longer resolves to this record + auto old_itr = sec.find("bob"_n.value); + sysio::check(old_itr == sec.end(), "s1secupd - bob should not exist in secondary index"); + + // Verify new key resolves correctly + auto new_itr = sec.find("zoe"_n.value); + sysio::check(new_itr != sec.end() && new_itr->id == 2, "s1secupd - zoe should map to id 2"); + } }; diff --git a/tests/unit/test_contracts/name_pk_tests.cpp b/tests/unit/test_contracts/name_pk_tests.cpp index 16f72be0a..6529cc1b2 100644 --- a/tests/unit/test_contracts/name_pk_tests.cpp +++ b/tests/unit/test_contracts/name_pk_tests.cpp @@ -1,6 +1,6 @@ // Verifies that a table with name-typed primary key works -#include +#include #include struct [[sysio::table]] name_table { @@ -9,7 +9,7 @@ struct [[sysio::table]] name_table { auto primary_key() const { return pk; } }; -using name_table_idx = sysio::multi_index<"name.pk"_n, name_table>; +using name_table_idx = sysio::kv_multi_index<"name.pk"_n, name_table>; class [[sysio::contract]] name_pk_tests : public sysio::contract { public: diff --git a/tools/codegen/cdt-codegen.cpp b/tools/codegen/cdt-codegen.cpp index 11776c949..059f4688a 100644 --- a/tools/codegen/cdt-codegen.cpp +++ b/tools/codegen/cdt-codegen.cpp @@ -60,10 +60,17 @@ static bool exists(const char* filename) { // (e.g. from SYSIO_DISPATCH) takes priority at link time. // When weak=false (standalone dispatch.cpp for link-mode builds), no weak attr. static void write_sysio_dispatch(std::ostream& ofs, const std::set& wasm_actions, - const std::set& wasm_notifies, bool weak) { + const std::set& wasm_notifies, bool weak, + bool has_pre_dispatch, bool has_post_dispatch) { ofs << "extern \"C\" {\n"; ofs << " __attribute__((import_name(\"sysio_assert_code\"))) void sysio_assert_code(uint32_t, uint64_t);"; ofs << " void sysio_set_contract_name(uint64_t n);\n"; + ofs << "}\n"; // close extern "C" for intrinsic declarations + if (has_pre_dispatch) + ofs << "extern \"C\" bool pre_dispatch(sysio::name, sysio::name, sysio::name);\n"; + if (has_post_dispatch) + ofs << "extern \"C\" void post_dispatch(sysio::name, sysio::name, sysio::name);\n"; + ofs << "extern \"C\" {\n"; for (auto& wa : wasm_actions) { ofs << " void " << wa.handler << "(uint64_t r, uint64_t c);\n"; } @@ -76,6 +83,8 @@ static void write_sysio_dispatch(std::ostream& ofs, const std::set& ofs << " __attribute__((export_name(\"apply\"), visibility(\"default\")))\n"; ofs << " void apply(uint64_t r, uint64_t c, uint64_t a) {\n"; ofs << " sysio_set_contract_name(r);\n"; + if (has_pre_dispatch) + ofs << " if (!pre_dispatch(sysio::name{r}, sysio::name{c}, sysio::name{a})) return;\n"; ofs << " if (c == r) {\n"; if (wasm_actions.size()) { ofs << " switch (a) {\n"; @@ -84,9 +93,14 @@ static void write_sysio_dispatch(std::ostream& ofs, const std::set& ofs << " " << wa.handler << "(r, c);\n"; ofs << " break;\n"; } - ofs << " default:\n" - << " if ( r != \"sysio\"_n.value) sysio_assert_code(false, 1);\n" - << " }\n"; + ofs << " default:\n"; + if (has_post_dispatch) { + ofs << " if (r != \"sysio\"_n.value) sysio_assert_code(false, 1);\n"; + ofs << " else post_dispatch(sysio::name{r}, sysio::name{c}, sysio::name{a});\n"; + } else { + ofs << " if (r != \"sysio\"_n.value) sysio_assert_code(false, 1);\n"; + } + ofs << " }\n"; } ofs << " } else {\n"; if (wasm_notifies.size()) { @@ -112,6 +126,10 @@ static void write_sysio_dispatch(std::ostream& ofs, const std::set& } ofs << " }\n"; ofs << " }\n"; + if (has_post_dispatch) + ofs << " else { post_dispatch(sysio::name{r}, sysio::name{c}, sysio::name{a}); }\n"; + } else if (has_post_dispatch) { + ofs << " post_dispatch(sysio::name{r}, sysio::name{c}, sysio::name{a});\n"; } ofs << " }\n"; ofs << " }\n"; @@ -120,14 +138,15 @@ static void write_sysio_dispatch(std::ostream& ofs, const std::set& // Generate a standalone dispatch.cpp file (for link-mode builds via cdt-cpp). static void generate_sysio_dispatch(const std::string& output, const std::set& wasm_actions, - const std::set& wasm_notifies) { + const std::set& wasm_notifies, + bool has_pre_dispatch, bool has_post_dispatch) { try { std::ofstream ofs(output); if (!ofs) throw std::runtime_error("cannot open " + output); ofs << "#include \n" << "#include \n"; - write_sysio_dispatch(ofs, wasm_actions, wasm_notifies, false); + write_sysio_dispatch(ofs, wasm_actions, wasm_notifies, false, has_pre_dispatch, has_post_dispatch); ofs.close(); } catch (...) { std::cerr << "Failed to generate sysio dispatcher\n"; @@ -412,6 +431,8 @@ int main(int argc, const char** argv) { std::set wasm_actions; std::set wasm_notifies; bool dispatcher_was_found = false; + bool has_pre_dispatch = false; + bool has_post_dispatch = false; parse_args(argc, argv); @@ -481,6 +502,10 @@ int main(int argc, const char** argv) { } } } + if (desc.has_key("has_pre_dispatch") && desc["has_pre_dispatch"].as_bool()) + has_pre_dispatch = true; + if (desc.has_key("has_post_dispatch") && desc["has_post_dispatch"].as_bool()) + has_post_dispatch = true; } } @@ -598,7 +623,7 @@ int main(int argc, const char** argv) { std::ofstream ofs(actions_file, std::ios::app); if (ofs) { ofs << "\n// --- Auto-generated weak dispatch ---\n"; - write_sysio_dispatch(ofs, wasm_actions, wasm_notifies, true); + write_sysio_dispatch(ofs, wasm_actions, wasm_notifies, true, has_pre_dispatch, has_post_dispatch); ofs.close(); } } @@ -606,7 +631,7 @@ int main(int argc, const char** argv) { } else { // Standalone dispatch.cpp for link-mode builds (cdt-cpp handles compilation). auto main_file = output_dir + "/" + contract_name + ".dispatch.cpp"; - generate_sysio_dispatch(main_file, wasm_actions, wasm_notifies); + generate_sysio_dispatch(main_file, wasm_actions, wasm_notifies, has_pre_dispatch, has_post_dispatch); } } return 0; diff --git a/tools/include/sysio/abi.hpp b/tools/include/sysio/abi.hpp index 0aa46ace7..a88d9be14 100644 --- a/tools/include/sysio/abi.hpp +++ b/tools/include/sysio/abi.hpp @@ -126,6 +126,8 @@ struct abi { std::set wasm_notifies; std::set wasm_entries; std::set action_results; + bool has_pre_dispatch = false; + bool has_post_dispatch = false; }; inline void dump( const abi& abi ) { diff --git a/tools/include/sysio/abimerge.hpp b/tools/include/sysio/abimerge.hpp index 6859a8198..1990b2a91 100644 --- a/tools/include/sysio/abimerge.hpp +++ b/tools/include/sysio/abimerge.hpp @@ -117,11 +117,14 @@ class ABIMerger { } static bool table_is_same(ojson a, ojson b) { + // key_names/key_types may differ: template-detected tables have them + // populated while attribute-only tables have empty arrays. Both are + // valid representations of the same table — treat as compatible. return a["name"] == b["name"] && a["type"] == b["type"] && a["index_type"] == b["index_type"] && - a["key_names"] == b["key_names"] && - a["key_types"] == b["key_types"]; + (a["key_names"] == b["key_names"] || + a["key_names"].empty() || b["key_names"].empty()); } static bool clause_is_same(ojson a, ojson b) { @@ -147,13 +150,17 @@ class ABIMerger { } for (auto obj_b : b[type].array_range()) { bool should_skip = false; - for (auto obj_a : a[type].array_range()) { - if (obj_a[id] == obj_b[id]) { - if (!is_same_func(obj_a, obj_b)) { - throw std::runtime_error(std::string("Error, ABI structs malformed : ")+obj_a[id].as()+" already defined"); + for (size_t i = 0; i < ret.size(); ++i) { + if (ret[i][id] == obj_b[id]) { + if (!is_same_func(ret[i], obj_b)) { + throw std::runtime_error(std::string("Error, ABI structs malformed : ")+ret[i][id].as()+" already defined"); } - else - should_skip = true; + // Prefer the entry with richer key metadata (non-empty key_names) + if (ret[i].count("key_names") && obj_b.count("key_names") && + ret[i]["key_names"].empty() && !obj_b["key_names"].empty()) { + ret[i] = obj_b; + } + should_skip = true; } } if (!should_skip)