Repository: rotki/rotki

Issue:
Update assets api search to non case-sensitive
For this api `/assets/all`, the search by `name` and `symbol` should be non case-senstive.

And also the check currently is `== search`. Can it be changed to `LIKE %search%`?


Discussion:

Resolving patch:
diff --git a/rotkehlchen/api/v1/schemas.py b/rotkehlchen/api/v1/schemas.py
--- a/rotkehlchen/api/v1/schemas.py
+++ b/rotkehlchen/api/v1/schemas.py
@@ -1821,6 +1821,7 @@ class AssetsSearchSchema(DBOrderBySchema, DBPaginationSchema):
         validate=webargs.validate.OneOf(choices=('name', 'symbol')),
     )
     return_exact_matches = fields.Boolean(load_default=False)
+    evm_chain = SerializableEnumField(enum_class=ChainID, load_default=None)
 
     @validates_schema
     def validate_schema(  # pylint: disable=no-self-use
@@ -1849,6 +1850,7 @@ def make_assets_search_query(  # pylint: disable=no-self-use
             substring_search=data['value'].strip(),
             search_column=data['search_column'],
             return_exact_matches=data['return_exact_matches'],
+            evm_chain=data['evm_chain'],
         )
         return {'filter_query': filter_query}
 
@@ -1866,6 +1868,7 @@ def make_assets_search_query(  # pylint: disable=no-self-use
         filter_query = AssetsFilterQuery.make(
             and_op=True,
             order_by_rules=create_order_by_rules_list(data=data, default_order_by_field='name'),
+            evm_chain=data['evm_chain'],
         )
         return {
             'filter_query': filter_query,
diff --git a/rotkehlchen/db/filtering.py b/rotkehlchen/db/filtering.py
--- a/rotkehlchen/db/filtering.py
+++ b/rotkehlchen/db/filtering.py
@@ -12,6 +12,7 @@
 from rotkehlchen.logging import RotkehlchenLogsAdapter
 from rotkehlchen.types import (
     AssetMovementCategory,
+    ChainID,
     ChecksumEvmAddress,
     EVMTxHash,
     Location,
@@ -429,7 +430,7 @@ def prepare(self) -> Tuple[List[str], List[Any]]:
 class DBEqualsFilter(DBFilter):
     """Filter a column by comparing its column to its value for equality."""
     column: str
-    value: Union[str, bytes]
+    value: Union[str, bytes, int]
     alias: Optional[str] = None
 
     def prepare(self) -> Tuple[List[str], List[Any]]:
@@ -931,6 +932,7 @@ def make(
             ignored_assets_identifiers: Optional[List[str]] = None,
             user_owned_assets_identifiers: Optional[List[str]] = None,
             return_exact_matches: bool = False,
+            evm_chain: Optional[ChainID] = None,
     ) -> 'AssetsFilterQuery':
         if order_by_rules is None:
             order_by_rules = [('name', True)]
@@ -946,16 +948,16 @@ def make(
 
         filters: List[DBFilter] = []
         if name is not None:
-            filters.append(DBEqualsFilter(
+            filters.append(DBSubStringFilter(
                 and_op=True,
-                column='name',
-                value=name,
+                field='name',
+                search_string=name,
             ))
         if symbol is not None:
-            filters.append(DBEqualsFilter(
+            filters.append(DBSubStringFilter(
                 and_op=True,
-                column='symbol',
-                value=symbol,
+                field='symbol',
+                search_string=symbol,
             ))
         if asset_type is not None:
             filters.append(DBEqualsFilter(
@@ -989,6 +991,12 @@ def make(
                 operator='NOT IN',
                 values=ignored_assets_identifiers,
             ))
+        if evm_chain is not None:
+            filters.append(DBEqualsFilter(
+                and_op=True,
+                column='chain',
+                value=evm_chain.serialize_for_db(),
+            ))
         filter_query.filters = filters
         return filter_query
 
diff --git a/rotkehlchen/globaldb/handler.py b/rotkehlchen/globaldb/handler.py
--- a/rotkehlchen/globaldb/handler.py
+++ b/rotkehlchen/globaldb/handler.py
@@ -305,13 +305,16 @@ def get_assets_mappings(identifiers: List[str]) -> Dict[str, dict]:
         identifiers_query = f'assets.identifier IN ({",".join("?" * len(identifiers))})'
         with GlobalDBHandler().conn.read_ctx() as cursor:
             cursor.execute(
-                'SELECT assets.identifier, assets.name, common_asset_details.symbol FROM assets '
-                'JOIN common_asset_details on assets.identifier = common_asset_details.identifier '
+                'SELECT assets.identifier, name, symbol, chain FROM assets '
+                'LEFT JOIN common_asset_details on assets.identifier = common_asset_details.identifier '  # noqa: E501
+                'LEFT JOIN evm_tokens ON evm_tokens.identifier= assets.identifier '
                 'WHERE ' + identifiers_query,
                 tuple(identifiers),
             )
             for entry in cursor:
                 result[entry[0]] = {'name': entry[1], 'symbol': entry[2]}
+                if entry[3] is not None:
+                    result[entry[0]].update({'evm_chain': ChainID.deserialize_from_db(entry[3]).serialize()})  # noqa: E501
             if len(result) != len(identifiers):
                 raise InputError('One or more of the given identifiers could not be found in the database')  # noqa: E501
         return result
@@ -346,7 +349,7 @@ def search_assets(
                     'symbol': entry[2],
                 }
                 if entry[3] is not None:
-                    entry_info['chain'] = ChainID.deserialize_from_db(entry[3]).serialize()
+                    entry_info['evm_chain'] = ChainID.deserialize_from_db(entry[3]).serialize()
 
                 search_result.append(entry_info)
         return search_result
@@ -409,7 +412,7 @@ def search_assets_levenshtein(
                         'symbol': entry[2],
                     }
                     if entry[3] is not None:
-                        entry_info['chain'] = ChainID.deserialize_from_db(entry[3]).serialize()
+                        entry_info['evm_chain'] = ChainID.deserialize_from_db(entry[3]).serialize()
                     search_result.append(entry_info)
                     levenshtein_distances.append(lev_dist_min)
 
@@ -421,7 +424,7 @@ def _prepare_search_assets_query(filter_query: 'AssetsFilterQuery') -> Tuple[str
         query, bindings = filter_query.prepare()
         parent_query = """
         SELECT assets.identifier, name, symbol, chain FROM assets
-        JOIN common_asset_details on assets.identifier = common_asset_details.identifier
+        LEFT JOIN common_asset_details on assets.identifier = common_asset_details.identifier
         LEFT JOIN evm_tokens ON evm_tokens.identifier= assets.identifier
         """
         query = parent_query + query


Test patch:
diff --git a/rotkehlchen/tests/api/test_assets.py b/rotkehlchen/tests/api/test_assets.py
--- a/rotkehlchen/tests/api/test_assets.py
+++ b/rotkehlchen/tests/api/test_assets.py
@@ -8,10 +8,11 @@
 import requests
 
 from rotkehlchen.accounting.structures.balance import BalanceType
-from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.asset import Asset, CustomAsset
 from rotkehlchen.assets.types import AssetType
 from rotkehlchen.balances.manual import ManuallyTrackedBalance
 from rotkehlchen.constants.assets import A_BTC, A_DAI, A_EUR, A_SAI, A_USD
+from rotkehlchen.db.custom_assets import DBCustomAssets
 from rotkehlchen.db.settings import ModifiableDBSettings
 from rotkehlchen.fval import FVal
 from rotkehlchen.globaldb import GlobalDBHandler
@@ -357,7 +358,7 @@ def test_get_all_assets(rotkehlchen_api_server):
     assert 'entries_total' in result
     assert 'entries_limit' in result
     for entry in result['entries']:
-        assert entry['name'] == 'Uniswap'
+        assert 'Uniswap' in entry['name']
     assert_asset_result_order(data=result['entries'], is_ascending=False, order_field='symbol')
 
     # test that ignored assets filter works
@@ -417,6 +418,55 @@ def test_get_all_assets(rotkehlchen_api_server):
     assert A_SAI.resolve_to_asset_with_name_and_type().name not in assets_names  # although present, it exceeds the limit  # noqa: E501
     assert_asset_result_order(data=result['entries'], is_ascending=True, order_field='name')
 
+    # add custom asset and filter results using `custom asset` type.
+    db_custom_assets = DBCustomAssets(
+        db_handler=rotkehlchen_api_server.rest_api.rotkehlchen.data.db,
+    )
+    custom_asset_id = str(uuid4())
+    db_custom_assets.add_custom_asset(CustomAsset.initialize(
+        identifier=custom_asset_id,
+        name='My Custom Prop',
+        custom_asset_type='random',
+    ))
+    response = requests.post(
+        api_url_for(
+            rotkehlchen_api_server,
+            'allassetsresource',
+        ),
+        json={
+            'limit': 10,
+            'offset': 0,
+            'order_by_attributes': ['name'],
+            'asset_type': 'custom asset',
+            'ascending': [True],
+        },
+    )
+    result = assert_proper_response_with_result(response)
+    assert len(result['entries']) == 1
+    assert result['entries'][0]['identifier'] == custom_asset_id
+    assert result['entries'][0]['type'] == 'custom asset'
+
+    # filter by name & symbol
+    response = requests.post(
+        api_url_for(
+            rotkehlchen_api_server,
+            'allassetsresource',
+        ),
+        json={
+            'limit': 50,
+            'offset': 0,
+            'name': 'Uniswap',
+            'symbol': 'UNI',
+            'order_by_attributes': ['symbol'],
+            'ascending': [False],
+        },
+    )
+    result = assert_proper_response_with_result(response)
+    assert 50 >= len(result['entries']) > 2
+    for entry in result['entries']:
+        assert 'Uniswap' in entry['name']
+        assert 'UNI' in entry['symbol']
+
     # check that providing multiple order_by_attributes fails
     response = requests.post(
         api_url_for(
@@ -436,7 +486,7 @@ def test_get_all_assets(rotkehlchen_api_server):
 
 def test_get_assets_mappings(rotkehlchen_api_server):
     """Test that providing a list of asset identifiers, the appropriate assets mappings are returned."""  # noqa: E501
-    queried_assets = ('BTC', 'TRY', 'EUR')
+    queried_assets = ('BTC', 'TRY', 'EUR', A_DAI.identifier)
     response = requests.post(
         api_url_for(
             rotkehlchen_api_server,
@@ -445,11 +495,15 @@ def test_get_assets_mappings(rotkehlchen_api_server):
         json={'identifiers': queried_assets},
     )
     result = assert_proper_response_with_result(response)
-    assert len(result) == 3
-    for identifier in result.keys():
+    assert len(result) == len(queried_assets)
+    for identifier, details in result.items():
         assert identifier in queried_assets
+        if identifier == A_DAI.identifier:
+            assert details['evm_chain'] == 'ethereum'
+        else:
+            assert 'evm_chain' not in details.keys()
 
-    # check that providing multiple order_by_attributes fails
+    # check that providing a wrong identifier fails
     response = requests.post(
         api_url_for(
             rotkehlchen_api_server,
@@ -567,9 +621,9 @@ def test_search_assets(rotkehlchen_api_server):
         assert entry['symbol'] == 'ETH'
         assert entry['identifier'] != 'ETH2'
         if entry['name'] != 'Binance-Peg Ethereum Token':
-            assert 'chain' not in entry
+            assert 'evm_chain' not in entry
         else:
-            assert entry['chain'] == 'binance'
+            assert entry['evm_chain'] == 'binance'
 
     assert_asset_result_order(data=result, is_ascending=True, order_field='name')
 
@@ -589,7 +643,7 @@ def test_search_assets(rotkehlchen_api_server):
     )
     assert_error_response(response, contained_in_msg='Must be one of: name, symbol.')
 
-    # test that the chain column is included
+    # test that the evm_chain column is included
     response = requests.post(
         api_url_for(
             rotkehlchen_api_server,
@@ -605,7 +659,44 @@ def test_search_assets(rotkehlchen_api_server):
         },
     )
     result = assert_proper_response_with_result(response)
-    assert {asset['chain'] for asset in result} == {'matic', 'optimism', 'ethereum', 'arbitrum', 'binance'}  # noqa: E501
+    assert {asset['evm_chain'] for asset in result} == {'matic', 'optimism', 'ethereum', 'arbitrum', 'binance'}  # noqa: E501
+
+    # check that using evm_chain filter works.
+    response = requests.post(
+        api_url_for(
+            rotkehlchen_api_server,
+            'assetssearchresource',
+        ),
+        json={
+            'value': 'DAI',
+            'search_column': 'symbol',
+            'limit': 50,
+            'evm_chain': 'ethereum',
+            'order_by_attributes': ['name'],
+            'ascending': [True],
+        },
+    )
+    result = assert_proper_response_with_result(response)
+    assert 50 >= len(result) > 10
+    assert all(['ethereum' == entry['evm_chain'] and 'DAI' in entry['symbol'] for entry in result])
+    assert_asset_result_order(data=result, is_ascending=True, order_field='name')
+
+    # check that using an unsupported evm_chain fails
+    response = requests.post(
+        api_url_for(
+            rotkehlchen_api_server,
+            'assetssearchresource',
+        ),
+        json={
+            'value': 'dai',
+            'search_column': 'symbol',
+            'limit': 50,
+            'evm_chain': 'near',
+            'order_by_attributes': ['name'],
+            'ascending': [True],
+        },
+    )
+    assert_error_response(response, contained_in_msg='Failed to deserialize ChainID value near')
 
 
 def test_search_assets_with_levenshtein(rotkehlchen_api_server):
@@ -699,3 +790,40 @@ def test_search_assets_with_levenshtein(rotkehlchen_api_server):
     )
     result = assert_proper_response_with_result(response)
     assert len(result) == 0
+
+    # check that using evm_chain filter works.
+    response = requests.post(
+        api_url_for(
+            rotkehlchen_api_server,
+            'assetssearchlevenshteinresource',
+        ),
+        json={
+            'value': 'dai',
+            'limit': 50,
+            'evm_chain': 'ethereum',
+        },
+    )
+    result = assert_proper_response_with_result(response)
+    assert 50 >= len(result) > 10
+    assert all(['ethereum' == entry['evm_chain'] for entry in result])
+    assert_substring_in_search_result(result, 'dai')
+    # check that Dai(DAI) appears at the top of result.
+    assert_asset_at_top_position(
+        asset_id=A_DAI.identifier,
+        max_position_index=0,
+        result=result,
+    )
+
+    # check that using an unsupported evm_chain fails
+    response = requests.post(
+        api_url_for(
+            rotkehlchen_api_server,
+            'assetssearchlevenshteinresource',
+        ),
+        json={
+            'value': 'dai',
+            'limit': 50,
+            'evm_chain': 'near',
+        },
+    )
+    assert_error_response(response, contained_in_msg='Failed to deserialize ChainID value near')