Repository: igorcoding/asynctnt (language: python)

Issue:
Support for decimal and float types for arithmetic operations in update
TypeError when trying to update a numeric or decimal field using an addition or subtraction operation using the Decimal or float type.

Example:
_init.lua_
```
box.cfg{listen = 3301}

space_list = box.space

require "os"

if not box.schema.user.exists(os.getenv("TARANTOOL_USER_NAME")) then
    box.schema.user.create(os.getenv("TARANTOOL_USER_NAME"), {password = os.getenv("TARANTOOL_USER_PASSWORD"),
                                             if_not_exists = true})
    box.schema.user.grant(os.getenv("TARANTOOL_USER_NAME"),'read,write,execute,create,drop','universe')
end

if not space_list['customer'] then
    s = box.schema.space.create('customer')

    s:format({
        {name = 'customer_id', type = 'unsigned'},
        {name = 'username', type = 'string', is_nullable=true},
        {name = 'is_active', type = 'boolean'},
        {name = 'balance', type = 'number', is_nullable=true}
    })

    s:create_index('primary', {
        type = 'hash',
        parts = {'customer_id'}
    })
end
```
_example.py_
```
import os
import asynctnt
import asyncio


async def main():
    conn = asynctnt.Connection(
        host='127.0.0.1',
        port=3301,
        username=os.getenv('TARANTOOL_USER_NAME'),
        password=os.getenv('TARANTOOL_USER_PASSWORD')
    )
    await conn.connect()

    result = await conn.update('customer', [1], [('+', 'balance', 23.65)])

    print(f'RESULT: {result}')

    await conn.disconnect()


asyncio.run(main())
```
_stderr_

>   ...
>   File "asynctnt\\iproto\\db.pyx", line 403, in asynctnt.iproto.protocol.Db.update
>   File "asynctnt\\iproto\\db.pyx", line 206, in asynctnt.iproto.protocol.Db._update
>   File "asynctnt\\iproto\\protocol.pyx", line 490, in asynctnt.iproto.protocol.BaseProtocol._execute_normal
>   File "asynctnt\\iproto\\requests/base.pyx", line 18, in asynctnt.iproto.protocol.BaseRequest.encode
>   File "asynctnt\\iproto\\requests/update.pyx", line 7, in asynctnt.iproto.protocol.UpdateRequest.encode_body
>   File "asynctnt\\iproto\\requests/update.pyx", line 220, in asynctnt.iproto.protocol.encode_request_update
>   File "asynctnt\\iproto\\requests/update.pyx", line 91, in asynctnt.iproto.protocol.encode_update_ops
>  TypeError: int argument required for Arithmetic and Delete operations


Discussion / hints:
Hi! Thank you for the issue! Sorry for the delay, I see the problem. As a workaround while the fix is not out yet I would suggest rewriting query as a `call`, something like this (didn't check the result, but essentially provide proper arguments as you will in the [call method](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/update/) in lua:
```python
await conn.call('box.space.customer:update', [1, [('+', 'balance', Decimal("23.65"))]])
```

Fix patch:
diff --git a/.github/workflows/actions.yaml b/.github/workflows/actions.yaml
index 61e68a2..21468d6 100644
--- a/.github/workflows/actions.yaml
+++ b/.github/workflows/actions.yaml
@@ -8,40 +8,44 @@ jobs:
       matrix:
         os: [ ubuntu-latest, macos-latest ]
         python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', 'pypy3.10']
-        tarantool: ['1.10', '2']
+        tarantool: ['1.10', '2', '3']
         exclude:
           - os: macos-latest
             tarantool: '1.10'
+          - os: macos-latest
+            tarantool: '2'
+          - os: macos-latest
+            python-version: '3.7'
           - python-version: 'pypy3.10'
             tarantool: '1.10'
 
     runs-on: ${{ matrix.os }}
 
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
           submodules: recursive
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install Tarantool ${{ matrix.tarantool }}
+
+      - name: Install Tarantool ${{ matrix.tarantool }} on Ubuntu
+        if: matrix.os == 'ubuntu-latest'
         run: |
-          if [ "$RUNNER_OS" == "Linux" ]; then
-            curl -L https://tarantool.io/nTmSHOX/release/${{ matrix.tarantool }}/installer.sh | bash
-            sudo apt-get -y install tarantool
-          elif [ "$RUNNER_OS" == "macOS" ]; then
-            brew install tarantool
-          else
-            echo "$RUNNER_OS not supported"
-            exit 1
-          fi
+          curl -L https://tarantool.io/nTmSHOX/release/${{ matrix.tarantool }}/installer.sh | bash
+          sudo apt-get -y install tarantool
+
+      - name: Install Tarantool ${{ matrix.tarantool }} on MacOS
+        if: matrix.os == 'macos-latest'
+        run: brew install tarantool
+
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip setuptools wheel coveralls
       - name: Run tests
         run: |
-          if [[ "$RUNNER_OS" == "Linux" && ${{ matrix.python-version }} == "3.12" && ${{ matrix.tarantool }} == "2" ]]; then
+          if [[ "$RUNNER_OS" == "Linux" && ${{ matrix.python-version }} == "3.12" && ${{ matrix.tarantool }} == "3" ]]; then
               make build && make test
               make clean && make debug && make coverage
               # coveralls
@@ -62,11 +66,11 @@ jobs:
       - test
 
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
           submodules: recursive
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
 
       - name: Install cibuildwheel
         run: python -m pip install --upgrade cibuildwheel
@@ -92,11 +96,11 @@ jobs:
         id: get_tag
         run: echo ::set-output name=TAG::${GITHUB_REF/refs\/tags\//}
       - run: echo "Current tag is ${{ steps.get_tag.outputs.TAG }}"
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
           submodules: recursive
       - name: Set up Python
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: '3.12'
 
@@ -104,7 +108,7 @@ jobs:
         run: |
           python -m pip install --upgrade pip setuptools wheel twine build
 
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: wheels
           path: wheels
@@ -133,12 +137,12 @@ jobs:
     needs:
       - test
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
           submodules: recursive
 
       - name: Set up Python
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: '3.12'
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3e1c3f..daef2f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
 # Changelog
 
+## v2.2.0
+**New features:**
+* Implemented ability to send update/upsert requests with field names when schema is disabled (`fetch_schema=False`) and when fields are not found in the schema (good example of this case is using json path like `data.inner1.inner2.key1` as a key)
+
+**Bug fixes:**
+* Fixed issue with not being able to send Decimals in update statements. Now there are no extra checks - any payload is sent directly to Tarantool (fixes [#34](https://github.com/igorcoding/asynctnt/issues/34))
+
+**Other changes**
+* Fixed tests failing on modern Tarantool in the SQL queries.
+* Removed from ci/cd testing on macOS python 3.7
+* Added Tarantool 3 to CI Testing
+
 ## v2.1.0
 **Breaking changes:**
 * Dropped support for Python 3.6
diff --git a/Makefile b/Makefile
index b1acef5..90e8798 100644
--- a/Makefile
+++ b/Makefile
@@ -45,7 +45,7 @@ mypy:
 	$(PYTHON) -m mypy --enable-error-code ignore-without-code .
 
 ruff:
-	$(PYTHON) -m ruff .
+	$(PYTHON) -m ruff check .
 
 style-check:
 	$(PYTHON) -m black --check --diff .
diff --git a/asynctnt/__init__.py b/asynctnt/__init__.py
index 9804faa..70a4944 100644
--- a/asynctnt/__init__.py
+++ b/asynctnt/__init__.py
@@ -16,4 +16,4 @@
     TarantoolTuple,
 )
 
-__version__ = "2.1.0"
+__version__ = "2.2.0"
diff --git a/asynctnt/iproto/requests/update.pyx b/asynctnt/iproto/requests/update.pyx
index f955e0c..e93a363 100644
--- a/asynctnt/iproto/requests/update.pyx
+++ b/asynctnt/iproto/requests/update.pyx
@@ -23,11 +23,17 @@ cdef char *encode_update_ops(WriteBuffer buffer,
 
         uint32_t extra_length
 
+        bint field_encode_as_str
         uint64_t field_no
+        char *field_str_c
+        ssize_t field_str_len
         object field_no_obj
 
         uint32_t splice_position, splice_offset
 
+    field_encode_as_str = 0
+    field_str_c = NULL
+
     begin = NULL
 
     if operations is not None:
@@ -60,22 +66,27 @@ cdef char *encode_update_ops(WriteBuffer buffer,
             raise TypeError(
                 'Operation type must of a str or bytes type')
 
+        cpython.bytes.PyBytes_AsStringAndSize(str_temp, &op_str_c,
+                                              &op_str_len)
+
         field_no_obj = operation[1]
         if isinstance(field_no_obj, int):
             field_no = <int> field_no_obj
         elif isinstance(field_no_obj, str):
             if space.metadata is not None:
-                field_no = <int> space.metadata.id_by_name(field_no_obj)
+                field_no = <int> space.metadata.id_by_name_safe(field_no_obj)
+                if field_no == -1:
+                    field_encode_as_str = 1
             else:
-                raise TypeError(
-                    'Operation field_no must be int as there is '
-                    'no format declaration in space {}'.format(space.sid))
+                field_encode_as_str = 1
+
+            if field_encode_as_str:
+                str_temp = encode_unicode_string(field_no_obj, buffer._encoding)
+                cpython.bytes.PyBytes_AsStringAndSize(str_temp, &field_str_c, &field_str_len)
         else:
             raise TypeError(
                 'Operation field_no must be of either int or str type')
 
-        cpython.bytes.PyBytes_AsStringAndSize(str_temp, &op_str_c,
-                                              &op_str_len)
         op = <char> 0
         if op_str_len == 1:
             op = op_str_c[0]
@@ -85,28 +96,9 @@ cdef char *encode_update_ops(WriteBuffer buffer,
                 or op == tarantool.IPROTO_OP_AND \
                 or op == tarantool.IPROTO_OP_XOR \
                 or op == tarantool.IPROTO_OP_OR \
-                or op == tarantool.IPROTO_OP_DELETE:
-            op_argument = operation[2]
-            if not isinstance(op_argument, int):
-                raise TypeError(
-                    'int argument required for '
-                    'Arithmetic and Delete operations'
-                )
-            # mp_sizeof_array(3)
-            # + mp_sizeof_str(1)
-            # + mp_sizeof_uint(field_no)
-            extra_length = 1 + 2 + mp_sizeof_uint(field_no)
-            p = begin = buffer._ensure_allocated(p, extra_length)
-
-            p = mp_encode_array(p, 3)
-            p = mp_encode_str(p, op_str_c, 1)
-            p = mp_encode_uint(p, field_no)
-            buffer._length += (p - begin)
-            p = buffer.mp_encode_obj(p, op_argument)
-        elif op == tarantool.IPROTO_OP_INSERT \
+                or op == tarantool.IPROTO_OP_DELETE \
+                or op == tarantool.IPROTO_OP_INSERT \
                 or op == tarantool.IPROTO_OP_ASSIGN:
-            op_argument = operation[2]
-
             # mp_sizeof_array(3)
             # + mp_sizeof_str(1)
             # + mp_sizeof_uint(field_no)
@@ -115,13 +107,19 @@ cdef char *encode_update_ops(WriteBuffer buffer,
 
             p = mp_encode_array(p, 3)
             p = mp_encode_str(p, op_str_c, 1)
-            p = mp_encode_uint(p, field_no)
+            if field_str_c == NULL:
+                p = mp_encode_uint(p, field_no)
+            else:
+                p = mp_encode_str(p, field_str_c, field_str_len)
+
             buffer._length += (p - begin)
+
+            op_argument = operation[2]
             p = buffer.mp_encode_obj(p, op_argument)
 
         elif op == tarantool.IPROTO_OP_SPLICE:
             if op_len < 5:
-                raise IndexError(
+                raise ValueError(
                     'Splice operation must have length of 5, '
                     'but got: {}'.format(op_len)
                 )
@@ -146,7 +144,10 @@ cdef char *encode_update_ops(WriteBuffer buffer,
 
             p = mp_encode_array(p, 5)
             p = mp_encode_str(p, op_str_c, 1)
-            p = mp_encode_uint(p, field_no)
+            if field_str_c == NULL:
+                p = mp_encode_uint(p, field_no)
+            else:
+                p = mp_encode_str(p, field_str_c, field_str_len)
             p = mp_encode_uint(p, splice_position)
             p = mp_encode_uint(p, splice_offset)
             buffer._length += (p - begin)
diff --git a/asynctnt/iproto/schema.pxd b/asynctnt/iproto/schema.pxd
index 60e09c2..a10885e 100644
--- a/asynctnt/iproto/schema.pxd
+++ b/asynctnt/iproto/schema.pxd
@@ -21,6 +21,7 @@ cdef public class Metadata [object C_Metadata, type C_Metadata_Type]:
     cdef inline void add(self, int id, Field field)
     cdef inline str name_by_id(self, int i)
     cdef inline int id_by_name(self, str name) except *
+    cdef inline int id_by_name_safe(self, str name) except*
 
 
 cdef class SchemaIndex:
diff --git a/asynctnt/iproto/schema.pyx b/asynctnt/iproto/schema.pyx
index b696194..a6e993e 100644
--- a/asynctnt/iproto/schema.pyx
+++ b/asynctnt/iproto/schema.pyx
@@ -51,6 +51,15 @@ cdef class Metadata:
             raise KeyError('Field \'{}\' not found'.format(name))
         return <int> <object> fld
 
+    cdef inline int id_by_name_safe(self, str name) except *:
+        cdef:
+            PyObject *fld
+
+        fld = cpython.dict.PyDict_GetItem(self.name_id_map, name)
+        if fld == NULL:
+            return -1
+        return <int> <object> fld
+
     cdef inline int len(self):
         return <int> cpython.list.PyList_GET_SIZE(self.fields)
 
diff --git a/pyproject.toml b/pyproject.toml
index 74c5c58..2716b58 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -120,7 +120,7 @@ skip_glob = [
 
 
 [tool.ruff]
-select = [
+lint.select = [
     "E",  # pycodestyle errors
     "W",  # pycodestyle warnings
     "F",  # pyflakes
@@ -128,7 +128,7 @@ select = [
     "C",  # flake8-comprehensions
     "B",  # flake8-bugbear
 ]
-ignore = [
+lint.ignore = [
     "E501",  # line too long, handled by black
     "B008",  # do not perform function calls in argument defaults
     "C901",  # too complex


Test patch:
diff --git a/tests/files/app.lua b/tests/files/app.lua
index ff2fb83..e5e64f2 100644
--- a/tests/files/app.lua
+++ b/tests/files/app.lua
@@ -51,6 +51,14 @@ local function bootstrap()
         types = {}
     }
 
+    function b:sql_space_name(space_name)
+        if self:check_version({3, 0}) then
+            return space_name
+        else
+            return space_name:upper()
+        end
+    end
+
     function b:check_version(expected)
         return check_version(expected, self.tarantool_ver)
     end
@@ -197,16 +205,22 @@ function truncate()
     _truncate(box.space.tester)
     _truncate(box.space.no_schema_space)
 
-    if box.space.SQL_SPACE ~= nil then
-        box.execute('DELETE FROM sql_space')
-    end
-
-    if box.space.SQL_SPACE_AUTOINCREMENT ~= nil then
-        box.execute('DELETE FROM sql_space_autoincrement')
-    end
-
-    if box.space.SQL_SPACE_AUTOINCREMENT_MULTIPLE ~= nil then
-        box.execute('DELETE FROM sql_space_autoincrement_multiple')
+    local sql_spaces = {
+        'sql_space',
+        'sql_space_autoincrement',
+        'sql_space_autoincrement_multiple',
+    }
+    for _, sql_space in ipairs(sql_spaces) do
+        local variants = {
+            sql_space,
+            sql_space:upper(),
+        }
+
+        for _, variant in ipairs(variants) do
+            if box.space[variant] ~= nil then
+                box.execute('DELETE FROM ' .. variant)
+            end
+        end
     end
 
     _truncate(box.space.tester_ext_dec)
diff --git a/tests/test_common.py b/tests/test_common.py
index cf36666..e57deb5 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -123,7 +123,9 @@ async def test__parse_numeric_map_keys(self):
     async def test__read_buffer_reallocate_ok(self):
         await self.tnt_reconnect(initial_read_buffer_size=1)
 
-        p, cmp = get_complex_param(encoding=self.conn.encoding)
+        p, cmp = get_complex_param(
+            encoding=self.conn.encoding, replace_bin=self.conn.version < (3, 0)
+        )
         try:
             res = await self.conn.call("func_param", [p])
         except Exception as e:
diff --git a/tests/test_op_call.py b/tests/test_op_call.py
index df7ed16..41e57be 100644
--- a/tests/test_op_call.py
+++ b/tests/test_op_call.py
@@ -79,12 +79,16 @@ async def test__call_args_tuple(self):
             self.fail(e)
 
     async def test__call_complex_param(self):
-        p, cmp = get_complex_param(encoding=self.conn.encoding)
+        p, cmp = get_complex_param(
+            encoding=self.conn.encoding, replace_bin=self.conn.version < (3, 0)
+        )
         res = await self.conn.call("func_param", [p])
         self.assertDictEqual(res[0][0], cmp, "Body ok")
 
     async def test__call_complex_param_bare(self):
-        p, cmp = get_complex_param(encoding=self.conn.encoding)
+        p, cmp = get_complex_param(
+            encoding=self.conn.encoding, replace_bin=self.conn.version < (3, 0)
+        )
         cmp = [cmp]
         res = await self.conn.call("func_param_bare", [p])
         if not self.has_new_call():
@@ -177,12 +181,16 @@ async def test__call16_args_tuple(self):
             self.fail(e)
 
     async def test__call16_complex_param(self):
-        p, cmp = get_complex_param(encoding=self.conn.encoding)
+        p, cmp = get_complex_param(
+            encoding=self.conn.encoding, replace_bin=self.conn.version < (3, 0)
+        )
         res = await self.conn.call("func_param", [p])
         self.assertDictEqual(res[0][0], cmp, "Body ok")
 
     async def test__call16_complex_param_bare(self):
-        p, cmp = get_complex_param(encoding=self.conn.encoding)
+        p, cmp = get_complex_param(
+            encoding=self.conn.encoding, replace_bin=self.conn.version < (3, 0)
+        )
         res = await self.conn.call16("func_param_bare", [p])
         self.assertDictEqual(res[0][0], cmp, "Body ok")
 
diff --git a/tests/test_op_eval.py b/tests/test_op_eval.py
index 868eff5..48fd72b 100644
--- a/tests/test_op_eval.py
+++ b/tests/test_op_eval.py
@@ -64,7 +64,9 @@ async def test__eval_args_tuple(self):
             self.fail(e)
 
     async def test__eval_complex_param(self):
-        p, cmp = get_complex_param(encoding=self.conn.encoding)
+        p, cmp = get_complex_param(
+            encoding=self.conn.encoding, replace_bin=self.conn.version < (3, 0)
+        )
         res = await self.conn.eval("return {...}", [p])
         self.assertDictEqual(res[0][0], cmp, "Body ok")
 
diff --git a/tests/test_op_sql_execute.py b/tests/test_op_sql_execute.py
index 5479eb8..574e7a6 100644
--- a/tests/test_op_sql_execute.py
+++ b/tests/test_op_sql_execute.py
@@ -1,9 +1,15 @@
+import asynctnt
 from asynctnt import Response
 from tests import BaseTarantoolTestCase
 from tests._testbase import ensure_version
 
 
 class SQLExecuteTestCase(BaseTarantoolTestCase):
+    def _compat_field_name(self, field_name: str) -> str:
+        if self.conn.version >= (3, 0):
+            return field_name
+        return field_name.upper()
+
     @ensure_version(min=(2, 0))
     async def test__sql_basic(self):
         res = await self.conn.execute("select 1, 2")
@@ -23,7 +29,16 @@ async def test__sql_with_param(self):
     async def test__sql_with_param_cols(self):
         res = await self.conn.execute("select 1 as a, 2 as b where 1 = ?", [1])
 
-        self.assertResponseEqualKV(res, [{"A": 1, "B": 2}], "Body ok")
+        self.assertResponseEqualKV(
+            res,
+            [
+                {
+                    self._compat_field_name("a"): 1,
+                    self._compat_field_name("b"): 2,
+                }
+            ],
+            "Body ok",
+        )
 
     @ensure_version(min=(2, 0))
     async def test__sql_with_param_cols2(self):
@@ -31,7 +46,16 @@ async def test__sql_with_param_cols2(self):
             "select 1 as a, 2 as b where 1 = ? and 2 = ?", [1, 2]
         )
 
-        self.assertResponseEqualKV(res, [{"A": 1, "B": 2}], "Body ok")
+        self.assertResponseEqualKV(
+            res,
+            [
+                {
+                    self._compat_field_name("a"): 1,
+                    self._compat_field_name("b"): 2,
+                }
+            ],
+            "Body ok",
+        )
 
     @ensure_version(min=(2, 0))
     async def test__sql_with_param_cols_maps(self):
@@ -43,16 +67,34 @@ async def test__sql_with_param_cols_maps(self):
             ],
         )
 
-        self.assertResponseEqualKV(res, [{"A": 1, "B": 2}], "Body ok")
+        self.assertResponseEqualKV(
+            res,
+            [
+                {
+                    self._compat_field_name("a"): 1,
+                    self._compat_field_name("b"): 2,
+                }
+            ],
+            "Body ok",
+        )
 
     @ensure_version(min=(2, 0))
     async def test__sql_with_param_cols_maps_and_positional(self):
         res = await self.conn.execute(
-            "select 1 as a, 2 as b " "where 1 = :p1 and 2 = :p2 and 3 = ? and 4 = ?",
+            "select 1 as a, 2 as b where 1 = :p1 and 2 = :p2 and 3 = ? and 4 = ?",
             [{":p1": 1}, {":p2": 2}, 3, 4],
         )
 
-        self.assertResponseEqualKV(res, [{"A": 1, "B": 2}], "Body ok")
+        self.assertResponseEqualKV(
+            res,
+            [
+                {
+                    self._compat_field_name("a"): 1,
+                    self._compat_field_name("b"): 2,
+                }
+            ],
+            "Body ok",
+        )
 
     @ensure_version(min=(2, 0))
     async def test__sql_insert(self):
@@ -111,24 +153,30 @@ async def test__sql_update_multiple(self):
 
     @ensure_version(min=(2, 0))
     async def test__sql_delete(self):
+        await self._compat(self.conn)
+
         await self.conn.execute("insert into sql_space values (1, 'one')")
         res = await self.conn.execute("delete from sql_space where name = 'one'")
         self.assertEqual(1, res.rowcount, "rowcount ok")
 
     @ensure_version(min=(2, 0))
     async def test__sql_select(self):
+        await self._compat(self.conn)
+
         await self.conn.execute("insert into sql_space values (1, 'one')")
         await self.conn.execute("insert into sql_space values (2, 'two')")
 
         res = await self.conn.execute("select * from sql_space")
         self.assertEqual(2, res.rowcount, "rowcount is surely ok")
-        self.assertEqual(1, res.body[0]["ID"])
-        self.assertEqual("one", res.body[0]["NAME"])
-        self.assertEqual(2, res.body[1]["ID"])
-        self.assertEqual("two", res.body[1]["NAME"])
+        self.assertEqual(1, res.body[0][self._compat_field_name("id")])
+        self.assertEqual("one", res.body[0][self._compat_field_name("name")])
+        self.assertEqual(2, res.body[1][self._compat_field_name("id")])
+        self.assertEqual("two", res.body[1][self._compat_field_name("name")])
 
     @ensure_version(min=(2, 0))
     async def test__sql_delete_multiple(self):
+        await self._compat(self.conn)
+
         await self.conn.execute("insert into sql_space values (1, 'one')")
         await self.conn.execute("insert into sql_space values (2, 'two')")
 
@@ -152,31 +200,35 @@ async def test__metadata(self):
 
     @ensure_version(min=(2, 0))
     async def test__metadata_names(self):
+        await self._compat(self.conn)
+
         res = await self.conn.execute("select 1 as a, 2 as b")
         self.assertIsNotNone(res.metadata)
         self.assertIsNotNone(res.metadata.fields)
         self.assertEqual(2, len(res.metadata.fields))
 
-        self.assertEqual("A", res.metadata.fields[0].name)
+        self.assertEqual(self._compat_field_name("a"), res.metadata.fields[0].name)
         self.assertEqual("integer", res.metadata.fields[0].type)
-        self.assertEqual("B", res.metadata.fields[1].name)
+        self.assertEqual(self._compat_field_name("b"), res.metadata.fields[1].name)
         self.assertEqual("integer", res.metadata.fields[1].type)
 
     @ensure_version(min=(2, 0))
     async def test__metadata_actual_space(self):
+        await self._compat(self.conn)
+
         await self.conn.execute("insert into sql_space values (1, 'one')")
         await self.conn.execute("insert into sql_space values (2, 'two')")
 
         res = await self.conn.execute("select * from sql_space")
         self.assertEqual(2, res.rowcount, "rowcount is ok")
         self.assertEqual(2, len(res.metadata.fields))
-        self.assertEqual("ID", res.metadata.fields[0].name)
+        self.assertEqual(self._compat_field_name("id"), res.metadata.fields[0].name)
         self.assertEqual("integer", res.metadata.fields[0].type)
         self.assertIsNone(res.metadata.fields[0].is_nullable)
         self.assertIsNone(res.metadata.fields[0].is_autoincrement)
         self.assertIsNone(res.metadata.fields[0].collation)
 
-        self.assertEqual("NAME", res.metadata.fields[1].name)
+        self.assertEqual(self._compat_field_name("name"), res.metadata.fields[1].name)
         self.assertEqual("string", res.metadata.fields[1].type)
         self.assertIsNone(res.metadata.fields[1].is_nullable)
         self.assertIsNone(res.metadata.fields[1].is_autoincrement)
@@ -184,6 +236,8 @@ async def test__metadata_actual_space(self):
 
     @ensure_version(min=(2, 0))
     async def test__sql_select_full_metadata(self):
+        await self._compat(self.conn)
+
         await self.conn.execute("insert into sql_space values (1, 'one')")
         await self.conn.execute("insert into sql_space values (2, 'two')")
 
@@ -194,13 +248,15 @@ async def test__sql_select_full_metadata(self):
         try:
             res = await self.conn.execute("select * from sql_space")
             self.assertEqual(2, len(res.metadata.fields))
-            self.assertEqual("ID", res.metadata.fields[0].name)
+            self.assertEqual(self._compat_field_name("id"), res.metadata.fields[0].name)
             self.assertEqual("integer", res.metadata.fields[0].type)
             self.assertEqual(False, res.metadata.fields[0].is_nullable)
             self.assertEqual(None, res.metadata.fields[1].is_autoincrement)
             self.assertIsNone(res.metadata.fields[0].collation)
 
-            self.assertEqual("NAME", res.metadata.fields[1].name)
+            self.assertEqual(
+                self._compat_field_name("name"), res.metadata.fields[1].name
+            )
             self.assertEqual("string", res.metadata.fields[1].type)
             self.assertEqual(True, res.metadata.fields[1].is_nullable)
             self.assertEqual(None, res.metadata.fields[1].is_autoincrement)
@@ -209,3 +265,7 @@ async def test__sql_select_full_metadata(self):
             await self.conn.update(
                 "_session_settings", ["sql_full_metadata"], [("=", "value", False)]
             )
+
+    async def _compat(self, conn: asynctnt.Connection):
+        if conn.version >= (2, 11):
+            await conn.execute('SET SESSION "sql_seq_scan" = true;')
diff --git a/tests/test_op_update.py b/tests/test_op_update.py
index 6d4bf82..ad71a7b 100644
--- a/tests/test_op_update.py
+++ b/tests/test_op_update.py
@@ -1,6 +1,9 @@
+from decimal import Decimal
+
 from asynctnt import Response
 from asynctnt.exceptions import ErrorCode, TarantoolDatabaseError, TarantoolSchemaError
 from tests import BaseTarantoolTestCase
+from tests._testbase import ensure_version
 
 
 class UpdateTestCase(BaseTarantoolTestCase):
@@ -9,6 +12,20 @@ async def _fill_data(self):
             [0, "a", 1, 5, "data1"],
             [1, "b", 8, 6, "data2"],
             [2, "c", 10, 12, "data3", "extra_field"],
+            [3, "d", 14, 16, "data4", Decimal("12.3"), 12.5],
+            [
+                4,
+                "e",
+                18,
+                20,
+                {
+                    "tree1": {
+                        "tree11": {
+                            "key1": "value1",
+                        }
+                    }
+                },
+            ],
         ]
         for t in data:
             await self.conn.insert(self.TESTER_SPACE_ID, t)
@@ -26,6 +43,26 @@ async def test__update_one_assign(self):
         data[1][2] = 2
         self.assertResponseEqual(res, [data[1]], "Body ok")
 
+    @ensure_version(min=(2, 3))
+    async def test__update_one_assign_by_field_name_with_no_schema(self):
+        data = await self._fill_data()
+
+        await self.tnt_reconnect(fetch_schema=False)
+
+        res = await self.conn.update(self.TESTER_SPACE_ID, [4], [["=", "f4", 100]])
+        data[4][3] = 100
+        self.assertResponseEqual(res, [data[4]], "Body ok")
+
+    @ensure_version(min=(2, 3))
+    async def test__update_one_assign_by_json(self):
+        data = await self._fill_data()
+
+        res = await self.conn.update(
+            self.TESTER_SPACE_ID, [4], [["=", "f5.tree1.tree11.key1", "value2"]]
+        )
+        data[4][4]["tree1"]["tree11"]["key1"] = "value2"
+        self.assertResponseEqual(res, [data[4]], "Body ok")
+
     async def test__update_one_insert(self):
         data = await self._fill_data()
 
@@ -47,6 +84,24 @@ async def test__update_one_plus(self):
         data[1][2] += 3
         self.assertResponseEqual(res, [data[1]], "Body ok")
 
+    @ensure_version(min=(2, 3))
+    async def test__update_one_plus_decimal(self):
+        data = await self._fill_data()
+
+        add = Decimal("1.1")
+        res = await self.conn.update(self.TESTER_SPACE_ID, [3], [["+", 5, add]])
+        data[3][5] += add
+        self.assertResponseEqual(res, [data[3]], "Body ok")
+
+    @ensure_version(min=(2, 3))
+    async def test__update_one_plus_float(self):
+        data = await self._fill_data()
+
+        add = 1.5
+        res = await self.conn.update(self.TESTER_SPACE_ID, [3], [["+", 6, add]])
+        data[3][6] += add
+        self.assertResponseEqual(res, [data[3]], "Body ok")
+
     async def test__update_one_plus_str_field(self):
         data = await self._fill_data()
 
@@ -54,10 +109,13 @@ async def test__update_one_plus_str_field(self):
         data[1][2] += 3
         self.assertResponseEqual(res, [data[1]], "Body ok")
 
+    @ensure_version(min=(2, 3))
     async def test__update_one_plus_str_field_unknown(self):
         await self._fill_data()
 
-        with self.assertRaisesRegex(KeyError, "Field 'f10' not found"):
+        with self.assertRaisesRegex(
+            TarantoolDatabaseError, "Field 'f10' was not found in the tuple"
+        ):
             await self.conn.update(self.TESTER_SPACE_ID, [1], [["+", "f10", 3]])
 
     async def test__update_one_plus_negative(self):
@@ -138,18 +196,16 @@ async def test__update_one_bxor(self):
         data[1][2] ^= 0
         self.assertResponseEqual(res, [data[1]], "Body ok")
 
+    @ensure_version(min=(2, 3))
     async def test__update_operations_not_int_without_schema(self):
         await self.tnt_reconnect(fetch_schema=False)
 
         data = [1, "hello2", 1, 4, "what is up"]
         await self.conn.insert(self.TESTER_SPACE_ID, data)
 
-        msg = (
-            "Operation field_no must be int as there is "
-            "no format declaration in space {}".format(self.TESTER_SPACE_ID)
-        )
-        with self.assertRaisesRegex(TypeError, msg):
-            await self.conn.update(self.TESTER_SPACE_ID, [1], [["+", "f3", 1]])
+        res = await self.conn.update(self.TESTER_SPACE_ID, [1], [["+", "f3", 1]])
+        data[2] += 1
+        self.assertResponseEqual(res, [data], "Body ok")
 
     async def test__update_splice(self):
         data = [1, "hello2", 1, 4, "what is up"]
@@ -179,12 +235,12 @@ async def test__update_splice_wrong_args(self):
             await self.conn.update(self.TESTER_SPACE_ID, [1], [[":", 2]])
 
         with self.assertRaisesRegex(
-            IndexError, r"Splice operation must have length of 5"
+            ValueError, r"Splice operation must have length of 5"
         ):
             await self.conn.update(self.TESTER_SPACE_ID, [1], [[":", 2, 1]])
 
         with self.assertRaisesRegex(
-            IndexError, r"Splice operation must have length of 5"
+            ValueError, r"Splice operation must have length of 5"
         ):
             await self.conn.update(self.TESTER_SPACE_ID, [1], [[":", 2, 1, 3]])
 
@@ -214,10 +270,12 @@ async def test__update_splice_wrong_args(self):
         ):
             await self.conn.update(self.TESTER_SPACE_ID, [1], [{}])
 
-        with self.assertRaisesRegex(
-            TypeError, r"int argument required for " r"Arithmetic and Delete operations"
-        ):
+        with self.assertRaises(TarantoolDatabaseError) as exc:
             await self.conn.update(self.TESTER_SPACE_ID, [1], [("+", 2, {})])
+        self.assertRegex(
+            exc.exception.message,
+            r"Argument type in operation '\+' on field \d does not match field type: expected a number",
+        )
 
     async def test__update_multiple_operations(self):
         t = [1, "1", 1, 5, "hello", 3, 4, 8]