Database Operations
On-chain data storage and retrieval is a critical feature of smart contracts. EOS implements an in-memory database that allows data to be stored in a table format. Each item in a table has a unique primary index, called a primary key, which is of type u64. The raw data stored in the table can be binary data of any length. When the storage function of a smart contract is called, the data is serialized and stored in the table. When reading, the stored data is deserialized back as a class object. Additionally, EOS supports secondary index tables of types u64, u128, u256, double, and long double, which can be considered special tables with a fixed data length. Primary and secondary index tables can be used together to implement multiple indexes. There can be multiple secondary index tables, and the values in these tables can be repeated, but the primary index in the primary index table must be unique.
The following example demonstrates the usage of EOS's in-memory database.
Store
Storage is the simplest function of the database, and the following code demonstrates this functionality.
# db_example1.codon
from chain.database import primary
from chain.contract import Contract
@table("mytable")
class A(object):
a: primary[u64]
b: str
def __init__(self, a: u64, b: str):
self.a = primary[u64](a)
self.b = b
@contract(main=True)
class MyContract(Contract):
def __init__(self):
super().__init__()
@action('teststore')
def test_store(self):
print('db_test')
item = A(123u64, 'hello, world')
table = A.new_table(n'hello', n'')
table.store(item, n'hello')
The code above is explained as follows:
-
In it, the line of code
@table("mytable")indicates the creation of a table type namedmytable. Note thatmytableis anametype.@tableinstructs the compiler to generate additional code related to table operations, like thenew_tablemethod above, which is generated by the compiler. -
A.new_table(n'hello', n'')creates a new table object,n'hello'is used to specify the account in which the table is stored, the second parameter represents the scope, which can generally be left empty. It is used in conjunction with the account name and table name to distinguish different tables. -
The line of code
table.store(item, n'hello')stores data in the on-chain database.n'hello'in it specifies that the RAM resources are paid through thehelloaccount, and it is necessary to have already signed with theactiveauthority of thehelloaccount in the Transaction.
Compile:
python-contract build db_example/db_example1.codon
ipyeos -m pytest -s -x test.py -k test_store
The test code you are running is as follows:
def test_store():
t = init_db_test('db_example1')
ret = t.push_action('hello', 'teststore', "", {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
Note:
In this example, if there is already data with the primary key 123u64 in the table, an exception will be thrown when the function is called.
To modify the above test case to the following code:
def test_example1():
t = init_db_test('db_example1')
ret = t.push_action('hello', 'teststore', "", {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
# will raise exception
ret = t.push_action('hello', 'teststore', "", {'hello': 'active'})
t.produce_block()
When running the test with the same command, if the push_action is called for the second time, the function will throw an exception similar to the following:
could not insert object, most likely a uniqueness constraint was violated
To avoid such exceptions, the update method must be used when updating data in the table. Before calling store, it is necessary to check whether the primary index already exists in the table. If it does, store method cannot be called, and update method must be used instead. The following example demonstrates how to use it:
find/update
This section demonstrates the lookup and update functionality of the database.
# db_example1.codon
...
@contract(main=True)
class MyContract(Contract):
...
@action('testupdate')
def test_update(self, value: str):
print('db_test')
table = A.new_table(n'hello', n'')
key = 123u64
it = table.find(key)
if it.is_ok():
print('+++++update value:', value)
item = A(key, value)
table.update(it, item, n'hello')
else:
print('+++++store value:', value)
item = A(key, value)
table.store(item, n'hello')
The following is the test code:
def test_update():
t = init_db_test('db_example1')
ret = t.push_action('hello', 'testupdate', {'value': 'hello, bob'}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
ret = t.push_action('hello', 'testupdate', {'value': 'hello, alice'}, {'hello': 'active'})
t.produce_block()
Compile using the following command:
python-contract build db_example/db_example1.codon
Execute the test code with the following command:
ipyeos -m pytest -s -x test.py -k test_update
When calling
t.push_action('hello', 'testupdate', {'value': 'hello, bob'}, {'hello': 'active'})
it will output:
+++++store value: hello, bob
When calling testupdate action again:
t.push_action('hello', 'testupdate', {'value': 'hello, alice'}, {'hello': 'active'})
it will output:
+++++update value: hello, alice
As you can see, the above code is a bit complicated. First, it needs to call find to determine whether the value corresponding to the primary index exists, and then decide whether to call store or update. It should be noted that, during the update process, the value of the primary index cannot be changed, otherwise an exception will be thrown.
You can try to modify the update code to:
item = A(key+1u64, value)
table.update(it, item, n'hello')
You will see an exception thrown in the smart contract.
Remove
The following code demonstrates how to remove an item from the database.
# db_example/db_example1.codon
@action('testremove')
def test_remove(self):
print('test remove')
item = A(123u64, 'hello, world')
table = A.new_table(n'hello', n'')
table.store(item, n'hello')
it = table.find(123u64)
assert it.is_ok()
table.remove(it)
it = table.find(123u64)
assert not it.is_ok()
Test code:
def test_remove():
t = init_db_test('db_example1')
ret = t.push_action('hello', 'testremove', "", {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
Compile using the following command:
python-contract build db_example/db_example1.codon
Test using the following command:
ipyeos -m pytest -s -x test.py -k test_remove
The above code first calls the store method to store the data with index 123u64 in the database, then calls remove to delete it, and uses assert to check the result. If everything is normal, the program will not throw any exceptions.
Lowerbound/Upperbound
These two methods are also used to search for elements in the database. Unlike the find method, these two functions are used for fuzzy searching. Among them, the lowerbound method returns an Iterator whose id is >= the specified id, and the upperbound method returns an Iterator whose id is > the specified id. Let's take a look at the usage below:
# db_example/db_example1.codon
...
@contract(main=True)
class MyContract(Contract):
...
@action('testbound')
def test_bound(self):
print('db_test')
table = A.new_table(n'hello', n'')
payer = n'hello'
value = A(1u64, "alice")
table.store(value, payer)
value = A(3u64, "bob")
table.store(value, payer)
value = A(5u64, "john")
table.store(value, payer)
it = table.lowerbound(1u64)
value2: A = it.get_value()
print("+++++:", value2.a, value2.b)
assert value2.a == 1u64 and value2.b == 'alice'
it = table.upperbound(1u64)
value2: A = it.get_value()
print("+++++:", value2.a, value2.b)
assert value2.a == 3u64 and value2.b == 'bob'
Test code:
def test_bound():
t = init_db_test('db_example4')
ret = t.push_action('hello', 'testbound', {}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
Compile using the following command:
python-contract build db_example/db_example1.codon
Run the test using the following command:
ipyeos -m pytest -s -x test.py -k test_bound
Output:
+++++: 1 alice
+++++: 3 bob
Querying the Primary Index of a Table Using API
The above examples are all about how to operate the database table on the chain through the smart contract. In fact, by using the get_table_rows API provided by EOS off the chain, you can also query the table on the chain.
In the test code, the definition of get_table_rows is as follows:
def get_table_rows(self, _json, code, scope, table,
lower_bound, upper_bound,
limit,
key_type='',
index_position='',
encode_type='',
reverse = False,
show_payer = False):
""" Fetch smart contract data from an account.
key_type: "i64"|"i128"|"i256"|"float64"|"float128"|"sha256"|"ripemd160"
index_position: "2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"|"10"
encode_type: "dec" or "hex", default to "dec"
"""
Let's break down the parameters for this function:
_json: If True, it returns JSON-formatted database records, if False, it returns the raw data in hexadecimal format.code: Represents the account where the table is located.scope: Generally set as an empty string. Whencodeandtableare the same, differentscopecan be used to differentiate different tables.table: The name of the table to be queried.lower_bound: The starting value of the primary or secondary index, specified bykey_type. It can be numeric, a string of numbers, or a hexadecimal string.upper_bound: The ending value of the primary or secondary index, specified bykey_type. It can be numeric, a string of numbers, or a hexadecimal string. If empty, it means no upper limit has been set. If a non-empty value is set, the results will return all values that are>=lower_boundand<=upper_bound.limit: Limits the number of returned values. If the queried records exceed the limit,morewill be set totruein the returned values, andnext_keywill point to the next valid index.key_type: The values can be:"name","i64","i128","i256","float64","float128","sha256","ripemd160". For the primary index (i.e.,index_positionis1), the value can only be"name"or"i64". For secondary index (i.e.,index_position >= 2), the value could be any of the listed types. The encoding method oflower_boundandupper_boundunder each value will be explained separately below.index_position: Specifies the relative position of the index. If it's empty or1, it denotes the primary index. Any number above2denotes the position of the secondary index.encode_type: It's either"dec"or"hex", defaulting to"dec". It specifies the encoding format oflower_bound,upper_bound, and the return valuenext_key.reverse: Specifies whether the data should be returned in reverse order.show_payer: Specifies whether to display the RAM resource paying account.
Detailed explanation for key_type:
- "name" is a
nametype string. - "i64" can be a numeric type or a string of numbers, such as 123 or "123".
- "i128" can be a numeric type, a string of numbers, or a hexadecimal string, such as: 123, "123", "0xaabb", "aabb".
- "i256" when the value of
encode_typeis"dec"or an empty string"", the encoding format is: a hexadecimal string, represented in little-endian mode, 64 characters in length. For example:fb54b91bfed2fe7fe39a92d999d002c550f0fa8360ec998f4bb65b00c86282f5will be converted into twouint128_ttype values in little-endian mode:50f0fa8360ec998f4bb65b00c86282f5andfb54b91bfed2fe7fe39a92d999d002c5. When the value ofencode_typeis"hex", it uses the same encoding method as the"sha256"type, which is big-endian mode. - "float64": The value is a floating-point string, like
"123.456". - "float128": When the value of
encode_typeis"dec"or an empty string"", the value is a floating-point string, like"123.456", and the range it represents can only be within the range allowed byfloat64. When the value ofencode_typeis"hex",encode_typerepresents the data as a hexadecimal string in little-endian mode. - "sha256": A hexadecimal string represented in big-endian mode, 64 characters long, will be converted into two
uint128_ttype values in little-endian mode: such asf58262c8005bb64b8f99ec6083faf050c502d099d9929ae37ffed2fe1bb954fbwill be converted into50f0fa8360ec998f4bb65b00c86282f5andfb54b91bfed2fe7fe39a92d999d002c5. Refer to the keytype_converter structure's code for more details. - "ripemd160": A hexadecimal string, 64 characters long, big-endian mode, will be converted into two
uint128_ttype values in little-endian mode: such as83a83a3876c64c33f66f33c54f1869edef5b5d4a000000000000000000000000will be converted intoed69184fc5336ff6334cc676383aa883and0000000000000000000000004a5d5bef. Refer to the keytype_converter structure's code for more details.
The get_table_rows function's parameters are quite complex, here's a summary:
- If
lower_boundandupper_boundare empty, it means the query has no range limit. - When the value of
key_typeis"i256"and"float128", the encoding method oflower_boundandupper_boundis also affected byencode_type.
First of all, to query a table using get_table_rows, the structure of the table must be visible in the ABI description. You can use the following code to describe the table in the corresponding generated ABI file:
# db_example5.codon
from chain.database import primary
from chain.contract import Contract
@table("mytable")
class A(object):
a: primary[u64]
b: str
def __init__(self, a: u64, b: str):
self.a = primary[u64](a)
self.b = b
@contract(main=True)
class MyContract(Contract):
def __init__(self):
super().__init__()
@action('test')
def test(self):
print('db_test')
table = A.new_table(n'hello', n'')
payer = n'hello'
value = A(1u64, "alice")
table.store(value, payer)
value = A(3u64, "bob")
table.store(value, payer)
value = A(5u64, "john")
table.store(value, payer)
it = table.lowerbound(1u64)
value2: A = it.get_value()
print("+++++:", value2.a, value2.b)
assert value2.a() == 1u64 and value2.b == 'alice'
it = table.upperbound(1u64)
value2: A = it.get_value()
print("+++++:", value2.a, value2.b)
assert value2.a() == 3u64 and value2.b == 'bob'
Here, the table decorator is used to make the compiler include the structure of the table in the ABI.
After adding this table decorator to the class, the compiler will automatically add the get_primary and new_table functions to the class.
At the same time, the member variables of the class must also meet certain requirements: first, a primary index variable must be declared, and the type must be database.primary. The implementation of the primary class is as follows:
class primary[T](object):
value: T
def __init__(self, value: T):
self.value = value
def get_primary(self) -> u64:
if isinstance(self.value, u64):
return self.value
return self.value.get_primary()
def __pack__(self, enc: Encoder):
self.value.__pack__(enc)
def __unpack__(dec: Decoder) -> primary[T]:
return primary[T](T.__unpack__(dec))
def __call__(self) -> T:
return self.value
def __size__(self) -> int:
return self.value.__size__()
The primary class is a template class. If the type of the value in primary is not of type u64, then the type must implement the get_primary method. The primary class also has a __call__ method to facilitate access to value. In the following discussion of multiple indexes, secondary indexes will also be used. The type of the secondary index must be database.secondary.
Compilation:
python-contract build db_example/db_example5.codon
You will see the following description in the generated db_example5.abi:
"tables": [
{
"name": "mytable",
"type": "A",
"index_type": "i64",
"key_names": [],
"key_types": []
}
]
Now consider the test code:
def test_example5():
t = init_db_test('db_example5')
ret = t.push_action('hello', 'test', {}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
rows = t.get_table_rows(True, 'hello', '', 'mytable', 1, '', 10)
logger.info('++++++=rows: %s', rows)
Run the test:
ipyeos -m pytest -s -x test.py -k test_example5
Output:
++++++=rows: {'rows': [{'a': 1, 'b': 'alice'}, {'a': 3, 'b': 'bob'}, {'a': 5, 'b': 'john'}], 'more': False, 'next_key': ''}
Operation of the secondary indexes
First, consider the following example:
# db_example7.codon
from chain.contract import Contract
from chain.database import primary, secondary
from chain.database import IdxTable64, IdxTable128, Iterator
from chain.name import Name
@table("mytable")
class A(object):
a: database.primary[u64]
b: secondary[u64]
c: secondary[u128]
def __init__(self, a: u64, b: u64, c: u128):
self.a = primary[u64](a)
self.b = secondary[u64](b)
self.c = secondary[u128](c)
@contract(main=True)
class MyContract(Contract):
def __init__(self):
super().__init__()
@action('test')
def test(self):
payer = n"hello"
table = A.new_table(n"hello", n"")
item = A(1u64, 2u64, 3u128)
table.store(item, payer)
idx_table_b = table.get_idx_table_by_b()
it = idx_table_b.find(2u64)
print("++++++it.primary:", it.primary)
assert it.primary == 1u64
idx_table_c = table.get_idx_table_by_c()
it = idx_table_c.find(3u128)
print("++++++it.primary:", it.primary)
assert it.primary == 1u64
In this example, two secondary indexes are defined:
b: secondary[u64]
c: secondary[u128]
In the code, get_idx_table_by_b and get_idx_table_by_c are used to obtain the tables of the secondary indexes, and the returned object types are IdxTable64 and IdxTable128, respectively. The tables of secondary indexes have similar method names as the tables of the primary index, and can also perform the function of secondary index lookup.
Test code:
# test.py
def test_example7():
t = init_db_test('db_example7')
ret = t.push_action('hello', 'test', {}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
Compile:
python-contract build db_example/db_example7.codon
Run the test:
ipyeos -m pytest -s -x test.py -k test_example7
Output:
++++++it.primary: 1
++++++it.primary: 1
Updating the secondary indexes
In practical applications, sometimes it is necessary to update the secondary index. Please first look at the following code:
# db_example8.codon
from chain.contract import Contract
from chain.database import primary, secondary
from chain.database import IdxTable64, IdxTable128, Iterator
from chain.name import Name
@table("mytable")
class A(object):
a: database.primary[u64]
b: secondary[u64]
c: secondary[u128]
def __init__(self, a: u64, b: u64, c: u128):
self.a = primary[u64](a)
self.b = secondary[u64](b)
self.c = secondary[u128](c)
@contract(main=True)
class MyContract(Contract):
def __init__(self):
super().__init__()
@action('test')
def test(self):
payer = n"hello"
table = A.new_table(n"hello", n"")
item = A(1u64, 2u64, 3u128)
table.store(item, payer)
item = A(111u64, 222u64, 333u128)
table.store(item, payer)
idx_table_b = table.get_idx_table_by_b()
it_sec = idx_table_b.find(2u64)
print("++++++it.primary:", it_sec.primary)
assert it_sec.primary == 1u64
table.update_b(it_sec, 22u64, payer)
it_sec = idx_table_b.find(22u64)
assert it_sec.is_ok()
print("++++++it.primary:", it_sec.primary)
assert it_sec.primary == 1u64
Note the following code in the above code:
idx_table_b = table.get_idx_table_by_b()
it_sec = idx_table_b.find(2u64)
print("++++++it.primary:", it_sec.primary)
assert it_sec.primary == 1u64
table.update_b(it_sec, 22u64, payer)
it_sec = idx_table_b.find(22u64)
assert it_sec.is_ok()
print("++++++it.primary:", it_sec.primary)
assert it_sec.primary == 1u64
Brief description of the process:
it_sec = idx_table_b.find(2u64): Looks up the value2u64in the secondary index and returns theSecondaryIteratortype resultit_sec.table.update_b(it_sec, 22u64, payer): This line of code implements the update function and updates the value ofbto22u64.it_sec = idx_table_b.find(22u64): Looks up the new secondary index.assert assert it_sec.is_ok(): Used to confirm whether the secondry index has been updated successfully.assert it_sec.primary == 1u64: Used to confirm whether the primary index is correct.
The update_b code is generated by the compiler and is shown below:
def update_b(self, it: SecondaryIterator, b: u64, payer: Name) -> None:
# Update the `b` secondary index.
self.idx_b.update(it, b, payer)
# Find the primary index.
it_primary = self.table.find(it.primary)
check(it_primary.is_ok(), "primary iterator not found")
# Get the value for the primary index.
value: A = it_primary.get_value()
# Update the value for the primary index.
value.b = secondary[u64](b)
self.table.update(it_primary, value, payer)
From the code, it is apparent that when updating the secondary index, the corresponding value in the primary index will also be updated.
Deleting the secondary index
@action('testremove')
def test_remove(self):
payer = n"hello"
table = A.new_table(n"hello", n"")
item = A(1u64, 2u64, 3u128)
table.store(item, payer)
idx_table_b = table.get_idx_table_by_b()
it_sec = idx_table_b.find(2u64)
assert it_sec.primary == 1u64
it = table.find(it_sec.primary)
table.remove(it)
it_sec = idx_table_b.find(2u64)
assert not it_sec.is_ok()
print('done!')
In this example, first call store to store an object A with the primary index of 1u64 and the first secondary index value of 2u64. Then query 2u64 and confirm that it_sec.primary == 1u64. Next, call remove to delete the data with the primary index of 1u64. Finally, query 2u64 again and confirm that the element has been deleted.
# test.py
def test_remove_secondary():
t = init_db_test('db_example8')
ret = t.push_action('hello', 'testremove', {}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
Compilation:
python-contract build db_example/db_example8.codon
Running the test:
ipyeos -m pytest -s -x test.py -k test_remove_secondary
Using the API to perform secondary index queries on a table
In the db_example8.codon example, two secondary indexes are defined, with the types u64 and u128, respectively. The get_table_rows API also supports finding corresponding values through secondary indexes.
def test_example9():
t = init_db_test('db_example8')
ret = t.push_action('hello', 'test', {}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
# find by secondary u64
rows = t.get_table_rows(True, 'hello', '', 'mytable', 22, '', 10, 'i64', '2')
logger.info("++++++++++%s", rows['rows'])
assert rows['rows'][0]['b'] == 22
# find by secondary u128
rows = t.get_table_rows(True, 'hello', '', 'mytable', '3', '', 10, 'i128', '3')
logger.info("++++++++++%s", rows['rows'])
assert rows['rows'][0]['c'] == '3'
Explanation of the code below:
To find the value in the table through the second index b:
rows = t.get_table_rows(True, 'hello', '', 'mytable', 22, '', 10, 'i64', '2')
Here, i64 is the index type of b, and 2 is zero-based index corresponding to the index.
To find the value in the table through the second index c:
rows = t.get_table_rows(True, 'hello', '', 'mytable', '3', '', 10, 'i128', '3')
Here, i128 is the index type of c. Note that the value 3 in the lowerbound parameter is the value of the secondary index. Since u128 has exceeded the range of 64-bit integers, a numeric string is used to represent it. Finally, the last parameter 3 is the corresponding index number.
The results of running the above test code are as follows:
++++++++++[{'a': 1, 'b': 22, 'c': '3'}, {'a': 111, 'b': 222, 'c': '333'}]
++++++++++[{'a': 1, 'b': 22, 'c': '3'}, {'a': 111, 'b': 222, 'c': '333'}]
Implementation principles of the database
The above code demonstrates the basic operations of the database. However, during the compilation process, some methods and classes are generated by the compiler. The following code displays the code generated by the compiler.
# db_example6.codon
from chain.contract import Contract
from chain.database import primary, secondary
from chain.database import IdxTable64, IdxTable128, Iterator
from chain.mi import MultiIndexBase
from chain.name import Name
@packer
class A(object):
a: database.primary[u64]
b: secondary[u64]
c: secondary[u128]
def __init__(self, a: u64, b: u64, c: u128):
self.a = primary[u64](a)
self.b = secondary[u64](b)
self.c = secondary[u128](c)
def get_primary(self) -> u64:
return self.a()
class MultiIndexA(MultiIndexBase[A]):
idx_b: IdxTable64
idx_c: IdxTable128
def __init__(self, code: Name, scope: Name, table: Name):
MultiIndexBase[A].__init__(code, scope, table)
idx_table_base = table.value & 0xfffffffffffffff0u64
self.idx_b = IdxTable64(0, code, scope, Name(idx_table_base | u64(0)))
self.idx_c = IdxTable128(1, code, scope, Name(idx_table_base | u64(1)))
def store(self, item: A, payer: Name) -> Iterator[A]:
id: u64 = item.get_primary()
it = self.table.store(item, payer)
self.idx_b.store(id, item.b(), payer)
self.idx_c.store(id, item.c(), payer)
return it
def update(self, it: Iterator[A], item: A, payer: Name):
self.table.update(it, item, payer)
primary = item.get_primary()
secondary = item.b()
it_secondary, old_secondary = self.idx_b.find_by_primary(primary)
if not secondary == old_secondary:
self.idx_b.update(it_secondary, secondary, payer)
secondary = item.c()
it_secondary, old_secondary = self.idx_c.find_by_primary(primary)
if not secondary == old_secondary:
self.idx_c.update(it_secondary, secondary, payer)
def remove(self, it: Iterator[A]):
sec_it, _ = self.idx_b.find_by_primary(it.get_primary())
self.idx_b.remove(sec_it)
sec_it, _ = self.idx_c.find_by_primary(it.get_primary())
self.idx_c.remove(sec_it)
self.table.remove(it)
def remove(self, primary: u64):
it = self.table.find(primary)
if it.is_ok():
self.remove(it)
def get_idx_table_by_b(self) -> IdxTable64:
return self.idx_b
def get_idx_table_by_c(self) -> IdxTable128:
return self.idx_c
def update_b(self, it: SecondaryIterator, b: u64, payer: Name) -> None:
self.idx_b.update(it, b, payer)
it_primary = self.table.find(it.primary)
check(it_primary.is_ok(), "primary iterator not found")
value: A = it_primary.get_value()
value.b = secondary[u64](b)
self.table.update(it_primary, value, payer)
def update_c(self, it: SecondaryIterator, c: u128, payer: Name) -> None:
self.idx_c.update(it, c, payer)
it_primary = self.table.find(it.primary)
check(it_primary.is_ok(), "primary iterator not found")
value: A = it_primary.get_value()
value.c = secondary[u128](c)
self.table.update(it_primary, value, payer)
@extend
class A:
def new_table(code: Name, scope: Name):
return MultiIndexA(code, scope, n"mytable")
@contract(main=True)
class MyContract(Contract):
def __init__(self):
super().__init__()
@action('test')
def test(self):
payer = n"hello"
table = A.new_table(n"hello", n"")
item = A(1u64, 2u64, 3u128)
table.store(item, payer)
idx_table_b = table.get_idx_table_by_b()
it = idx_table_b.find(2u64)
print("++++++it.primary:", it.primary)
assert it.primary == 1u64
idx_table_c = table.get_idx_table_by_c()
it = idx_table_c.find(3u128)
print("++++++it.primary:", it.primary)
assert it.primary == 1u64
This example demonstrates a scenario where there are two secondary indexes. Only the name table was changed to packer. In this case, the compiler will not generate any code related to the database.
By comparison, it is apparent that the compiler generates a class named MultiIndexA, which inherits from the MultiIndexBase class defined in mi.codon. This class has the following methods:
- def store(self, item: A, payer: Name) -> Iterator[A]
- def update(self, it: Iterator[A], item: A, payer: Name)
- def remove(self, it: Iterator[A])
- def get_idx_table_by_b(self) -> IdxTable64:
- def get_idx_table_by_c(self) -> IdxTable128:
- def update_b(self, it: SecondaryIterator, b: u64, payer: Name) -> None:
- def update_c(self, it: SecondaryIterator, c: u128, payer: Name) -> None:
In addition, the class A is generated, along with the following additional methods:
get_primary: retrieves the primary indexget_idx_table_by_b: retrieves the table indexed byb, returning an instance of theIdxTable64classget_idx_table_by_c: retrieves the table indexed byc, returning an instance of theIdxTable128classnew_table
Test code:
def test_example6():
t = init_db_test('db_example6')
ret = t.push_action('hello', 'test', {}, {'hello': 'active'})
t.produce_block()
logger.info("++++++++++%s\n", ret['elapsed'])
Compilation:
python-contract build db_example/db_example6.codon
Running the test:
ipyeos -m pytest -s -x test.py -k test_example6
Output:
++++++it.primary: 1
++++++it.primary: 1
Summary
The data storage functionality in EOS is relatively comprehensive, and the secondary index table function makes data querying very flexible. This chapter provides a detailed explanation of the code for table operations, including adding, deleting, modifying, and querying. This chapter contains a lot of content and requires some time to digest. You can try to modify the examples and run them to gain a better understanding of the content covered in this chapter.