Repository: igorcoding/asynctnt (language: python)

Issue:
no running event loop
I can't define connection and start functions without running the event loop.
```python
if PY_37:
    __get_running_loop = asyncio.get_running_loop  # by the reason you can't use the module without a running event loop
```
I want the connection variable to be global and access functions from other modules while the event loop is not running yet
conn = asynctnt.Connection(host='127.0.0.1', port=3301)

At the moment I have to make a crutch out of the initial function:
```python
async def init_conn():
    global conn
    conn = asynctnt.Connection(host='127.0.0.1', port=3301)
    await asyncio.sleep(0)
```
so that I can access functions from other modules.

the full example which doesn't work without a crutch:
```python
import asyncio
import asynctnt

conn = asynctnt.Connection(host='127.0.0.1', port=3301)

async def insert(space: str, data, replace=False):
    if not conn.is_connected:
        await conn.connect()
    result = await conn.insert(space, list(data), replace=replace)
    return result[0] if len(result) == 1 else result


async def update(space: str, key, ops):
    if not conn.is_connected:
        await conn.connect()
    key = [key] if type(key) is not list else key
    result = await conn.update(space, key, ops)
    return result[0] if len(result) > 0 else None


async def select(space: str, key=None, index='primary', unpack=True):
    if not conn.is_connected:
        await conn.connect()
    key = [key] if type(key) is not list and key else key
    result = await conn.select(space, key=key, index=index)
    return result[0] if len(result) == 1 and unpack else result


async def sql(query, args=None, parse_metadata=False):
    if not conn.is_connected:
        await conn.connect()
    result = await conn.sql(query, args, parse_metadata=parse_metadata)
    return result


async def delete(space: str, key):
    if not conn.is_connected:
        await conn.connect()
    key = [key] if type(key) is not list else key
    await conn.delete(space, key)
```


Discussion / hints:
Hi!
Yeah, I know this became not very convenient for a simple use. This happened because of the support of Python 3.8 (they are ditching all the `loop` parameters from the entire asyncio). 
But I myself was considering to still allow what you are describing, I'll try to work on a fix in a couple of days.
btw - there is no need in asyncio.sleep(0).

Fix patch:
diff --git a/.flake8 b/.flake8
index b0f7518..50c7254 100644
--- a/.flake8
+++ b/.flake8
@@ -1,4 +1,4 @@
 [flake8]
-filename = *.pyx,*.px*,*.py
+filename = *.pyx, *.pxd, *.pxi, *.py, *.pyi
 exclude = .eggs,*.egg,build,env*,temp,asynctnt/__init__.py
-ignore = E999,E225,E227,E402,E127,E226,W503
+ignore = E999,E225,E227,E402,E127,E226,W503,E704
diff --git a/.github/workflows/actions.yaml b/.github/workflows/actions.yaml
index a918cca..977f427 100644
--- a/.github/workflows/actions.yaml
+++ b/.github/workflows/actions.yaml
@@ -8,10 +8,12 @@ jobs:
       matrix:
         os: [ ubuntu-latest, macos-latest ]
         python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']
-        tarantool: ['1.10', '2.7']
+        tarantool: ['1.10', '2.7', '2']
         exclude:
           - os: macos-latest
             tarantool: '1.10'
+          - os: macos-latest
+            tarantool: '2.7'
 
     runs-on: ${{ matrix.os }}
 
@@ -40,7 +42,7 @@ jobs:
           python -m pip install -r requirements.txt
       - name: Run tests
         run: |
-          if [[ "$RUNNER_OS" == "Linux" && ${{ matrix.python-version }} == "3.10" && ${{ matrix.tarantool }} == "2.7" ]]; then
+          if [[ "$RUNNER_OS" == "Linux" && ${{ matrix.python-version }} == "3.10" && ${{ matrix.tarantool }} == "2" ]]; then
               make && make test
               make clean && make debug && make test PYTHON='coverage run'
               # coveralls
diff --git a/.gitignore b/.gitignore
index 58ac62e..701b16c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -100,8 +100,9 @@ ENV/
 *.xlog
 *.xctl
 protocol.c
+protocol.cpp
 protocol.h
-asynctnt/iproto/*.html
+asynctnt/**/*.html
 __tnt*
 !third_party/**/*
 vinyl.meta
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d38e9aa..f44d465 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,37 @@
-## v.1.2.3
+# Changelog
+
+## v2.0.0
+**Breaking changes:**
+* `Connection.sql()` method is renamed to `Connection.execute()`
+* Drop support for `loop` argument in the `Connection` (fixes [#18](https://github.com/igorcoding/asynctnt/issues/18))
+
+**New features:**
+* Added support for `Decimal`, `UUID` and `datetime` types natively using MessagePack extensions
+* Added support for SQL prepared statements with `Connection.prepare()` method and
+  `PreparedStatement` class
+* Added support for interactive transactions and streams (fixes [#21](https://github.com/igorcoding/asynctnt/issues/21))
+* Added support for MP_ERROR extensions
+* Bind metadata of parameters is available now in the `response.params` and `response.params_count` fields
+* Exposed an internal schema as a `Connection.schema` property, introducing new classes to operate the schema with
+* Exposed SQL metadata of responses as `response.metadata` field
+* Added typings to internal types such as `Connection`, `Response`, `Metadata`, `Schema`, `TarantoolTuple` and others
+* `asynctnt` now sends IPROTO_ID request before anything else to notify Tarantool of used features
+
+**Other changes:**
+* Updated Cython to 0.29.30
+* Update msgpuck to revision 0c6680a300e31714f475a7f90c2d95a02d001d80
+* Internal refactoring of requests payload encoding
+* Refactoring of schema parsing and unifying under `metadata` name and structure
+
+## v1.2.3
 * Support Python 3.10
 
-## v.1.2.2
+## v1.2.2
 **Bugs fixed:**
-* Show a diag message rather than Lost connection to Tarantool when disconnected (closes #19)
+* Show a diag message rather than Lost connection to Tarantool when disconnected (closes [#19](https://github.com/igorcoding/asynctnt/issues/19))
 
 
-## v.1.2.1
+## v1.2.1
 
 **Other changes:**
 * Updated Cython to 0.29.21
@@ -14,11 +39,11 @@
 
 ## v1.2
 **Bugs fixed:**
-* Fixed hanging PushIterator when connection to Tarantool is lost (#17).
+* Fixed hanging PushIterator when connection to Tarantool is lost ([#17](https://github.com/igorcoding/asynctnt/issues/17)).
 
 ## v1.1
 **New features:**
-* Parse autoincrement ids in sql response (#14). Thanks to @oleynikandrey
+* Parse autoincrement ids in sql response ([#14](https://github.com/igorcoding/asynctnt/issues/14)). Thanks to @oleynikandrey
 * Added Python 3.8 support. Removed all redundant `loop` arguments to functions
   and asyncio classes.
 
@@ -34,7 +59,7 @@
 
 **New features:**
 * Making Response objects contain TarantoolTuple objects if format information
-  is available either in space or in response from Tarantool (closes #3).
+  is available either in space or in response from Tarantool (closes [#3](https://github.com/igorcoding/asynctnt/issues/3)).
 * TarantoolTuple objects are index-agnostic, meaning one can access tuple value
   either by numeric index or by a key from `space:format()` specification.
 * You can directly access Response using indices
@@ -60,7 +85,7 @@
   in the tuple. One can acces those extra fields by index numbers.
 * `Connection`'s default `connect_timeout` changed from `60` to `3` seconds.
 * `select`: changed default iterator type to `ALL` if no key provided
-  (fixes #2)
+  (fixes [#2](https://github.com/igorcoding/asynctnt/issues/2))
 * `Response` new function `done()` indicates if Response is
   actually finished.
 * `schema_id` is not being sent to Tarantool to check against current schema
@@ -87,7 +112,7 @@
 * Added disconnect Lock
 
 **Bugs Fixed:**
-* Auto reconnect misbehaved on double on_connection_lost trigger (#11)
+* Auto reconnect misbehaved on double on_connection_lost trigger ([#11](https://github.com/igorcoding/asynctnt/issues/11))
 
 
 ## v0.1.13
@@ -100,9 +125,9 @@
 **Bugs fixed:**
 * Connect hanged indefinitely if asynctnt was accidentally disconnected from
   Tarantool and TCP connection was still alive for a moment while trying to
-  reconnect (#8).
+  reconnect ([#8](https://github.com/igorcoding/asynctnt/issues/8)).
 * Connect to LOADING Tarantool instance without username/password resulted in
-  exception NO_SUCH_SPACE (#10).
+  exception NO_SUCH_SPACE ([#10](https://github.com/igorcoding/asynctnt/issues/10)).
 
 
 ## v0.1.12
diff --git a/Makefile b/Makefile
index 39a0f1a..3428bc6 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: build debug test coverage clean annotate all
+.PHONY: build debug test coverage clean annotate all style
 
 
 PYTHON?=python
@@ -8,10 +8,10 @@ all: build
 
 
 clean:
-	rm -rf asynctnt/*.c asynctnt/*.h
+	rm -rf asynctnt/*.c asynctnt/*.h asynctnt/*.cpp
 	rm -rf asynctnt/*.so asynctnt/*.html
 	rm -rf asynctnt/iproto/*.c asynctnt/iproto/*.h
-	rm -rf asynctnt/iproto/*.so asynctnt/iproto/*.html
+	rm -rf asynctnt/iproto/*.so asynctnt/iproto/*.html asynctnt/iproto/requests/*.html
 	rm -rf build *.egg-info .eggs dist
 	find . -name '__pycache__' | xargs rm -rf
 	rm -rf htmlcov
diff --git a/README.md b/README.md
index 9a03f59..60bb492 100644
--- a/README.md
+++ b/README.md
@@ -8,10 +8,10 @@
 </a>
 
 asynctnt is a high-performance [Tarantool](https://tarantool.org/) database
-connector library for Python/asyncio. It is highly inspired by
+connector library for Python/asyncio. It was highly inspired by
 [asyncpg](https://github.com/MagicStack/asyncpg) module.
 
-asynctnt requires Python 3.5 or later and is supported for Tarantool
+asynctnt requires Python 3.6 or later and is supported for Tarantool
 versions 1.6+.
 
 
@@ -29,14 +29,16 @@ Documentation is available [here](https://igorcoding.github.io/asynctnt).
 
 ## Key features
 
-* Support for all of the basic requests that Tarantool supports. This includes:
-  `insert`, `select`, `update`, `upsert`, `eval`, `sql` (for Tarantool 2.x),
-  `call` and `call16`. _Note: For the difference between `call16` and `call`
-  please refer to Tarantool documentation._
+* Support for all the **basic requests** that Tarantool supports. This includes:
+  `insert`, `select`, `update`, `upsert`, `call`, `eval`, `execute`.
+* Full support for [SQL](https://www.tarantool.io/en/doc/latest/tutorials/sql_tutorial/),
+  including [prepared statements](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_sql/prepare/).
+* Support for [interactive transaction](https://www.tarantool.io/en/doc/latest/book/box/atomic/txn_mode_mvcc/) via Tarantool streams.
+* Support of `Decimal`, `UUID` and `datetime` types natively.
+* Support for parsing [custom errors](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_error/new/).
 * **Schema fetching** on connection establishment, so you can use spaces and
-  indexes names rather than their ids.
-* Schema **auto refetching**. If schema in Tarantool is changed, `asynctnt`
-  refetches it.
+  indexes names rather than their ids, and **auto refetching** if schema in
+  Tarantool is changed
 * **Auto reconnect**. If connection is lost for some reason - asynctnt will
   start automatic reconnection procedure (with authorization and schema
   fetching, of course).
@@ -47,9 +49,7 @@ Documentation is available [here](https://igorcoding.github.io/asynctnt).
   into the special structures that can act either as `tuple`s or by `dict`s with
   the appropriate API.
 * All requests support specification of `timeout` value, so if request is
-  executed for too long, asyncio.TimeoutError is raised. It drastically
-  simplifies your code, as you don't need to use `asyncio.wait_for(...)`
-  stuff anymore.
+  executed for too long, asyncio.TimeoutError is raised.
 
 
 ## Basic Usage
@@ -69,12 +69,15 @@ box.once('v1', function()
     s:format({
         { name = 'id', type = 'unsigned' },
         { name = 'name', type = 'string' },
+        { name = 'uuid', type = 'uuid' },
     })
 end)
 ```
 
 Python code:
+
 ```python
+import uuid
 import asyncio
 import asynctnt
 
@@ -84,16 +87,18 @@ async def main():
     await conn.connect()
 
     for i in range(1, 11):
-        await conn.insert('tester', [i, 'hello{}'.format(i)])
+        await conn.insert('tester', [i, 'hello{}'.format(i), uuid.uuid4()])
 
     data = await conn.select('tester', [])
-    first_tuple = data[0]
-    print('tuple:', first_tuple)
-    print(f'tuple[0]: {first_tuple[0]}; tuple["id"]: {first_tuple["id"]}')
-    print(f'tuple[1]: {first_tuple[1]}; tuple["name"]: {first_tuple["name"]}')
+    tup = data[0]
+    print('tuple:', tup)
+    print(f'{tup[0]=}; {tup["id"]=}')
+    print(f'{tup[1]=}; {tup["name"]=}')
+    print(f'{tup[2]=}; {tup["uuid"]=}')
 
     await conn.disconnect()
 
+
 asyncio.run(main())
 ```
 
@@ -102,15 +107,16 @@ Stdout:
 *(note that you can simultaneously access fields either by indices
 or by their names)*
 ```
-tuple: <TarantoolTuple id=1 name='hello1'>
-tuple[0]: 1; tuple["id"]: 1
-tuple[1]: hello1; tuple["name"]: hello1
+tuple: <TarantoolTuple id=1 name='hello1' uuid=UUID('ebbad14c-f78c-42e8-bd12-bfcc564443a6')>
+tup[0]=1; tup["id"]=1
+tup[1]='hello1'; tup["name"]='hello1'
+tup[2]=UUID('ebbad14c-f78c-42e8-bd12-bfcc564443a6'); tup["uuid"]=UUID('ebbad14c-f78c-42e8-bd12-bfcc564443a6')
 ```
 
 ## SQL
 
-Tarantool 2 brings out an SQL interface to the database. You can easily use SQL
-through `asynctnt`
+Tarantool 2.x brought out an SQL interface to the database. You can easily use it
+in `asynctnt`
 
 ```lua
 box.cfg {
@@ -138,9 +144,9 @@ async def main():
     conn = asynctnt.Connection(host='127.0.0.1', port=3301)
     await conn.connect()
 
-    await conn.sql("insert into users (id, name) values (1, 'James Bond')")
-    await conn.sql("insert into users (id, name) values (2, 'Ethan Hunt')")
-    data = await conn.sql('select * from users')
+    await conn.execute("insert into users (id, name) values (?, ?)", [1, 'James Bond'])
+    await conn.execute("insert into users (id, name) values (?, ?)", [2, 'Ethan Hunt'])
+    data = await conn.execute('select * from users')
 
     for row in data:
         print(row)
@@ -156,62 +162,36 @@ Stdout:
 <TarantoolTuple ID=2 NAME='Ethan Hunt'>
 ```
 
-## Performance
-
-On all of the benchmarks below `wal_mode = none`
-
-### Sequential
-
-RPS on running 40k requests (no `uvloop`):
-
-| Request       | aiotarantool  | asynctnt  |
-| ------------- |:-------------:| ---------:|
-| ping          | 5010.60       | 9037.07   |
-| call          | 4575.98       | 9113.32   |
-| eval          | 4096.32       | 8921.95   |
-| select        | 4063.15       | 9681.12   |
-| insert        | 4038.04       | 9332.21   |
-| update        | 3945.12       | 10532.75  |
-
+More about SQL features in asynctnt please refer to the [documentation](https://igorcoding.github.io/asynctnt/sql.html)
 
-RPS on running 40k requests (with `uvloop`):
-
-| Request       | aiotarantool  | asynctnt  |
-| ------------- |:-------------:| ---------:|
-| ping          | 7204.31       | 20372.59  |
-| call          | 6723.58       | 17279.21  |
-| eval          | 7001.27       | 16642.67  |
-| select        | 7028.03       | 17730.24  |
-| insert        | 7054.06       | 17384.26  |
-| update        | 6618.01       | 15990.12  |
-
-
-### Parallel coroutines
-
-RPS on running 200k requests in 300 parallel coroutines (no `uvloop`):
+## Performance
 
-| Request       | aiotarantool  | asynctnt  |
-| ------------- |:-------------:| ---------:|
-| ping          | 32946.25      | 44090.53  |
-| call          | 29005.93      | 41129.16  |
-| eval          | 28792.84      | 44097.02  |
-| select        | 26929.76      | 35853.33  |
-| insert        | 27142.52      | 31329.85  |
-| update        | 25330.98      | 36281.59  |
+Two performance tests were conducted:
+1. `Seq` -- Sequentially calling 40k requests and measuring performance
+2. `Parallel` -- Sending 200k in 300 parallel coroutines
 
+On all the benchmarks below `wal_mode = none`.
+Turning `uvloop` on has a massive effect on the performance, so it is recommended to use `asynctnt` with it
 
-Let's enable uvloop. This is where asynctnt shines.
-RPS on running 200k requests in 300 parallel coroutines (with `uvloop`):
+**Benchmark environment**
+* MacBook Pro 2020
+* CPU: 2 GHz Quad-Core Intel Core i5
+* Memory: 16GB 3733 MHz LPDDR4X
 
+Tarantool:
+```lua
+box.cfg{wal_mode = 'none'}
+```
 
-| Request       | aiotarantool  | asynctnt   |
-| ------------- |:-------------:| ----------:|
-| ping          | 38962.01      | 134043.41  |
-| call          | 32799.71      | 99866.28   |
-| eval          | 27608.09      | 91056.69   |
-| select        | 27436.98      | 108940.41  |
-| insert        | 33247.57      | 102971.13  |
-| update        | 28544.68      | 98643.46   |
+|           |  Seq (uvloop=off) | Seq (uvloop=on) | Parallel (uvloop=off) | Parallel (uvloop=on) |
+|-----------|------------------:|----------------:|----------------------:|---------------------:|
+| `ping`    |          12940.93 |        19980.82 |              88341.95 |            215756.24 |
+| `call`    |          11586.38 |        18783.56 |              74651.40 |            137557.25 |
+| `eval`    |          10631.19 |        17040.57 |              61077.84 |            121542.42 |
+| `select`  |           9613.88 |        16718.97 |              61584.07 |            152526.21 |
+| `insert`  |          10077.10 |        16989.06 |              65594.82 |            135491.25 |
+| `update`  |          10832.16 |        16562.80 |              63003.31 |            121892.28 |
+| `execute` |          10431.75 |        16967.85 |              58377.81 |             96891.61 |
 
 
 ## License
diff --git a/asynctnt/__init__.py b/asynctnt/__init__.py
index bfd9bc1..10fafed 100644
--- a/asynctnt/__init__.py
+++ b/asynctnt/__init__.py
@@ -1,6 +1,8 @@
 from .connection import Connection, connect
 from .iproto.protocol import (
-    Iterator, Response, TarantoolTuple, PushIterator
+    Iterator, Response, TarantoolTuple, PushIterator,
+    Schema, SchemaSpace, SchemaIndex, Metadata, Field,
+    Db, IProtoError, IProtoErrorStackFrame
 )
 
-__version__ = '1.2.3'
+__version__ = '2.0.0'
diff --git a/asynctnt/api.py b/asynctnt/api.py
new file mode 100644
index 0000000..01b166a
--- /dev/null
+++ b/asynctnt/api.py
@@ -0,0 +1,435 @@
+import enum
+from typing import Optional, List, Any, Union, Dict
+
+from asynctnt.iproto import protocol
+
+from .exceptions import TarantoolNotConnectedError
+from .prepared import PreparedStatement
+from .types import MethodRet, SpaceType, KeyType, TupleType
+
+
+class _DbMock:
+    def __getattr__(self, item):
+        raise TarantoolNotConnectedError('Tarantool is not connected')
+
+
+class Isolation(enum.IntEnum):
+    DEFAULT = 0
+    READ_COMMITTED = 1
+    READ_CONFIRMED = 2
+    BEST_EFFORT = 3
+
+
+class Api:
+    __slots__ = (
+        '_db',
+    )
+
+    def __init__(self):
+        self._db: Union[_DbMock, protocol.Db] = _DbMock()
+
+    def _set_db(self, db: protocol.Db):
+        self._db = db
+
+    def _clear_db(self):
+        self._db = _DbMock()
+
+    def ping(self, *, timeout: float = -1.0) -> MethodRet:
+        """
+            Ping request coroutine
+
+            :param timeout: Request timeout
+
+            :returns: :class:`asynctnt.Response` instance
+        """
+        return self._db.ping(timeout=timeout)
+
+    def call16(self,
+               func_name: str,
+               args: Optional[List[Any]] = None,
+               *,
+               timeout: float = -1.0,
+               push_subscribe: bool = False) -> MethodRet:
+        """
+            Call16 request coroutine. It is a call with an old behaviour
+            (return result of a Tarantool procedure is wrapped into a tuple,
+            if needed)
+
+            :param func_name: function name to call
+            :param args: arguments to pass to the function (list object)
+            :param timeout: Request timeout
+            :param push_subscribe: Subscribe to push notifications
+
+            :returns: :class:`asynctnt.Response` instance
+        """
+        return self._db.call16(func_name, args,
+                               timeout=timeout,
+                               push_subscribe=push_subscribe)
+
+    def call(self,
+             func_name: str,
+             args: Optional[List[Any]] = None,
+             *,
+             timeout: float = -1.0,
+             push_subscribe: bool = False) -> MethodRet:
+        """
+            Call request coroutine. It is a call with a new behaviour
+            (return result of a Tarantool procedure is not wrapped into
+            an extra tuple). If you're connecting to Tarantool with
+            version < 1.7, then this call method acts like a call16 method
+
+            Examples:
+
+            .. code-block:: pycon
+
+                # tarantool function:
+                # function f(...)
+                #     return ...
+                # end
+
+                >>> await conn.call('f')
+                <Response sync=3 rowcount=0 data=[]>
+
+                >>> await conn.call('f', [20, 42])
+                <Response sync=3 rowcount=2 data=[20, 42]>
+
+            :param func_name: function name to call
+            :param args: arguments to pass to the function (list object)
+            :param timeout: Request timeout
+            :param push_subscribe: Subscribe to push notifications
+
+            :returns: :class:`asynctnt.Response` instance
+        """
+        return self._db.call(func_name, args,
+                             timeout=timeout, push_subscribe=push_subscribe)
+
+    def eval(self,
+             expression: str,
+             args: Optional[List[Any]] = None,
+             *,
+             timeout: float = -1.0,
+             push_subscribe: bool = False) -> MethodRet:
+        """
+            Eval request coroutine.
+
+            Examples:
+
+            .. code-block:: pycon
+
+                >>> await conn.eval('return 42')
+                <Response sync=3 rowcount=1 data=[42]>
+
+
+                >>> await conn.eval('return box.info.version')
+                <Response sync=3 rowcount=1 data=['2.1.1-7-gd381a45b6']>
+
+            :param expression: expression to execute
+            :param args: arguments to pass to the function, that will
+                         execute your expression (list object)
+            :param timeout: Request timeout
+            :param push_subscribe: Subscribe to push messages
+
+            :returns: :class:`asynctnt.Response` instance
+        """
+        return self._db.eval(expression, args,
+                             timeout=timeout, push_subscribe=push_subscribe)
+
+    def select(self,
+               space: SpaceType,
+               key: Optional[KeyType] = None,
+               **kwargs) -> MethodRet:
+        """
+            Select request coroutine.
+
+            Examples:
+
+            .. code-block:: pycon
+
+                >>> await conn.select('tester')
+                <Response sync=3 rowcount=2 data=[
+                    <TarantoolTuple id=1 name='one'>,
+                    <TarantoolTuple id=2 name='two'>
+                ]>
+
+                >>> res = await conn.select('_space', ['tester'], index='name')
+                >>> res.data
+                [<TarantoolTuple id=512
+                                 owner=1
+                                 name='tester'
+                                 engine='memtx'
+                                 field_count=0
+                                 flags={}
+                                 format=[
+                                    {'name': 'id', 'type': 'unsigned'},
+                                    {'name': 'name', 'type': 'string'}
+                                 ]>]
+
+
+            :param space: space id or space name.
+            :param key: key to select
+            :param offset: offset to use
+            :param limit: limit to use
+            :param index: index id or name
+            :param iterator: one of the following
+
+                        * iterator id (int number),
+                        * :class:`asynctnt.Iterator` object
+                        * string with an iterator name
+
+            :param timeout: Request timeout
+
+            :returns: :class:`asynctnt.Response` instance
+        """
+        return self._db.select(space, key, **kwargs)
+
+    def insert(self,
+               space: SpaceType,
+               t: TupleType,
+               *,
+               replace: bool = False,
+               timeout: float = -1) -> MethodRet:
+        """
+            Insert request coroutine.
+
+            Examples:
+
+            .. code-block:: pycon
+
+                # Basic usage
+                >>> await conn.insert('tester', [0, 'hello'])
+                <Response sync=3 rowcount=1 data=[
+                    <TarantoolTuple id=0 name='hello'>
+                ]>
+
+                # Using dict as an argument tuple
+                >>> await conn.insert('tester', {
+                ...                     'id': 0
+                ...                     'text': 'hell0'
+                ...                   })
+                <Response sync=3 rowcount=1 data=[
+                    <TarantoolTuple id=0 name='hello'>
+                ]>
+
+            :param space: space id or space name.
+            :param t: tuple to insert (list object)
+            :param replace: performs replace request instead of insert
+            :param timeout: Request timeout
+
+            :returns: :class:`asynctnt.Response` instance
+        """
+        return self._db.insert(space, t,
+                               replace=replace,
+                               timeout=timeout)
+
+    def replace(self,
+                space: SpaceType,
+                t: TupleType,
+                *,
+                timeout: float = -1.0) -> MethodRet:
+        """
+            Replace request coroutine. Same as insert, but replace.
+
+            :param space: space id or space name.
+            :param t: tuple to insert (list object)
+            :param timeout: Request timeout
+
+            :returns: :class:`asynctnt.Response` instance
+        """
+        return self._db.replace(space, t, timeout=timeout)
+
+    def delete(self,
+               space: SpaceType,
+               key: KeyType,
+               **kwargs) -> MethodRet:
+        """
+            Delete request coroutine.
+
+            Examples:
+
+            .. code-block:: pycon
+
+                # Assuming tuple [0, 'hello'] is in space tester
+
+                >>> await conn.delete('tester', [0])
+                <Response sync=3 rowcount=1 data=[
+                    <TarantoolTuple id=0 name='hello'>
+                ]>
+
+            :param space: space id or space name.
+            :param key: key to delete
+            :param index: index id or name
+            :param timeout: Request timeout
+
+            :returns: :class:`asynctnt.Response` instance
+        """
+        return self._db.delete(space, key, **kwargs)
+
+    def update(self,
+               space: SpaceType,
+               key: KeyType,
+               operations: List[Any],
+               **kwargs) -> MethodRet:
+        """
+            Update request coroutine.
+
+            Examples:
+
+            .. code-block:: pycon
+
+                # Assuming tuple [0, 'hello'] is in space tester
+
+                >>> await conn.update('tester', [0], [ ['=', 1, 'hi!'] ])
+                <Response sync=3 rowcount=1 data=[
+                    <TarantoolTuple id=0 name='hi!'>
+                ]>
+
+                # you can use fields names as well
+                >>> res = await conn.update('tester', [0],
+                ...                         [ ['=', 'text', 'hola'] ])
+                <Response sync=3 rowcount=1 data=[
+                    <TarantoolTuple id=0 name='hola'>
+                ]>
+
+            :param space: space id or space name.
+            :param key: key to update
+            :param operations:
+                    Operations list of the following format:
+                    [ [op_type, field_no, ...], ... ]. Please refer to
+                    https://tarantool.org/doc/book/box/box_space.html?highlight=update#lua-function.space_object.update
+                    You can use field numbers as well as their names in space
+                    format as a field_no (if only fetch_schema is True).
+                    If field is unknown then TarantoolSchemaError is raised.
+            :param index: index id or name
+            :param timeout: Request timeout
+
+            :returns: :class:`asynctnt.Response` instance
+        """
+        return self._db.update(space, key, operations, **kwargs)
+
+    def upsert(self,
+               space: SpaceType,
+               t: TupleType,
+               operations: List[Any],
+               **kwargs) -> MethodRet:
+        """
+            Update request coroutine. Performs either insert or update
+            (depending of either tuple exists or not)
+
+            Examples:
+
+            .. code-block:: pycon
+
+                # upsert does not return anything
+                >>> await conn.upsert('tester', [0, 'hello'],
+                ...                   [ ['=', 1, 'hi!'] ])
+                <Response sync=3 rowcount=0 data=[]>
+
+            :param space: space id or space name.
+            :param t: tuple to insert if it's not in space
+            :param operations:
+                    Operations list to use for update if tuple is already in
+                    space. It has the same format as in update requets:
+                    [ [op_type, field_no, ...], ... ]. Please refer to
+                    https://tarantool.org/doc/book/box/box_space.html?highlight=update#lua-function.space_object.update
+                    You can use field numbers as well as their names in space
+                    format as a field_no (if only fetch_schema is True).
+                    If field is unknown then TarantoolSchemaError is raised.
+            :param timeout: Request timeout
+
+            :returns: :class:`asynctnt.Response` instance
+        """
+        return self._db.upsert(space, t, operations, **kwargs)
+
+    def execute(self,
+                query: Union[str, int],
+                args: Optional[List[Union[Dict[str, Any], Any]]] = None, *,
+                parse_metadata: bool = True,
+                timeout: float = -1.0) -> MethodRet:
+        """
+            Executes an SQL statement (only for Tarantool > 2)
+
+            Examples:
+
+            .. code-block:: pycon
+
+                >>> await conn.execute("select 1 as a, 2 as b")
+                <Response sync=3 rowcount=1 data=[<TarantoolTuple A=1 B=2>]>
+
+                >>> await conn.execute("select * from sql_space")
+                <Response sync=3 rowcount=2 data=[
+                    <TarantoolTuple ID=1 NAME='James Bond'>,
+                    <TarantoolTuple ID=2 NAME='Ethan Hunt'>
+                ]>
+
+                >>> await conn.execute("select * from sql_space",
+                ...                    parse_metadata=False)
+                <Response sync=3 rowcount=2 data=[
+                    <TarantoolTuple 0=1 1='James Bond'>,
+                    <TarantoolTuple 0=2 1='Ethan Hunt'>
+                ]>
+
+            :param query: SQL query or statement_id
+            :param args: Query arguments
+            :param parse_metadata: Set to False to disable response's metadata
+                                   parsing for better performance
+            :param timeout: Request timeout
+
+            :returns: :class:`asynctnt.Response` instance
+        """
+        return self._db.execute(query, args,
+                                parse_metadata=parse_metadata,
+                                timeout=timeout)
+
+    def prepare(self, query: str) -> PreparedStatement:
+        """
+            Create a :class:`asynctnt.prepared.PreparedStatement` instance
+            :param query: query to be prepared
+        """
+        return PreparedStatement(self, query)
+
+    def prepare_iproto(self,
+                       query: str,
+                       timeout: float = -1.0) -> MethodRet:
+        """
+            Low-level prepare() call
+            :param query: query to be prepared
+            :param timeout: request timeout
+        """
+        return self._db.prepare(query, timeout=timeout)
+
+    def unprepare_iproto(self,
+                         stmt_id: int,
+                         timeout: float = -1.0) -> MethodRet:
+        """
+            Low-level unprepare() call
+            :param stmt_id: query to be unprepared
+            :param timeout: request timeout
+        """
+        return self._db.prepare(stmt_id, timeout=timeout)
+
+    def begin(self,
+              isolation: Isolation = Isolation.DEFAULT,
+              tx_timeout: float = 0.0,
+              timeout: float = -1.0) -> MethodRet:
+        """
+        Begin an interactive transaction within a stream
+        :param isolation: isolation level
+        :param tx_timeout: transaction timeout
+        :param timeout: request timeout
+        :return:
+        """
+        return self._db.begin(isolation.value, tx_timeout, timeout)
+
+    def commit(self, timeout: float = -1.0) -> MethodRet:
+        """
+        Commit a running transaction
+        :param timeout: request timeout
+        """
+        return self._db.commit(timeout)
+
+    def rollback(self, timeout: float = -1.0) -> MethodRet:
+        """
+        Rollback a running transaction
+        :param timeout: request timeout
+        """
+        return self._db.rollback(timeout)
diff --git a/asynctnt/connection.py b/asynctnt/connection.py
index f51f719..5ad289d 100644
--- a/asynctnt/connection.py
+++ b/asynctnt/connection.py
@@ -2,20 +2,20 @@
 import enum
 import functools
 import os
-import typing
-from typing import Optional
+from typing import Optional, Union
 
+from .api import Api
 from .exceptions import TarantoolDatabaseError, \
-    ErrorCode, TarantoolNotConnectedError
+    ErrorCode, TarantoolError
 from .iproto import protocol
 from .log import logger
+from .stream import Stream
+from .utils import get_running_loop
 
 __all__ = (
     'Connection', 'connect', 'ConnectionState'
 )
 
-from .utils import get_running_loop
-
 
 class ConnectionState(enum.IntEnum):
     CONNECTING = 1
@@ -25,35 +25,31 @@ class ConnectionState(enum.IntEnum):
     DISCONNECTED = 5
 
 
-_MethodRet = typing.Union[typing.Awaitable[protocol.Response], asyncio.Future]
-
-
-class Connection:
+class Connection(Api):
     __slots__ = (
         '_host', '_port', '_username', '_password',
         '_fetch_schema', '_auto_refetch_schema', '_initial_read_buffer_size',
         '_encoding', '_connect_timeout', '_reconnect_timeout',
         '_request_timeout', '_ping_timeout', '_loop', '_state', '_state_prev',
-        '_transport', '_protocol', '_db',
+        '_transport', '_protocol',
         '_disconnect_waiter', '_reconnect_task',
         '_connect_lock', '_disconnect_lock',
         '_ping_task', '__create_task'
     )
 
     def __init__(self, *,
-                 host='127.0.0.1',
-                 port=3301,
-                 username=None,
-                 password=None,
-                 fetch_schema=True,
-                 auto_refetch_schema=True,
-                 connect_timeout=3.,
-                 request_timeout=-1.,
-                 reconnect_timeout=1. / 3.,
-                 ping_timeout=5.,
-                 encoding=None,
-                 initial_read_buffer_size=None,
-                 **kwargs):
+                 host: str = '127.0.0.1',
+                 port: Union[int, str] = 3301,
+                 username: Optional[str] = None,
+                 password: Optional[str] = None,
+                 fetch_schema: bool = True,
+                 auto_refetch_schema: bool = True,
+                 connect_timeout: float = 3.,
+                 request_timeout: float = -1.,
+                 reconnect_timeout: float = 1. / 3.,
+                 ping_timeout: float = 5.,
+                 encoding: Optional[str] = None,
+                 initial_read_buffer_size: Optional[int] = None):
 
         """
             Connection constructor.
@@ -116,9 +112,8 @@ def __init__(self, *,
                     Initial and minimum size of read buffer in bytes.
                     Higher value means less reallocations, but higher
                     memory usage (default is 131072).
-            :param loop:
-                    (Deprecated) Asyncio event loop to use
         """
+        super().__init__()
         self._host = host
         self._port = port
         self._username = username
@@ -140,11 +135,11 @@ def __init__(self, *,
         self._request_timeout = request_timeout
         self._ping_timeout = ping_timeout or 0
 
-        self._loop = get_running_loop(kwargs.pop('loop', None))
+        self._loop = None
+        self.__create_task = None
 
         self._transport = None
-        self._protocol = None
-        self._db = _DbMock()
+        self._protocol: Optional[protocol.Protocol] = None
 
         self._state = ConnectionState.DISCONNECTED
         self._state_prev = ConnectionState.DISCONNECTED
@@ -154,12 +149,7 @@ def __init__(self, *,
         self._disconnect_lock = asyncio.Lock()
         self._ping_task = None
 
-        if hasattr(self._loop, 'create_task'):
-            self.__create_task = self._loop.create_task
-        else:
-            self.__create_task = asyncio.ensure_future
-
-    def _set_state(self, new_state):
+    def _set_state(self, new_state: ConnectionState):
         if self._state != new_state:
             logger.debug('Changing state %s -> %s',
                          self._state.name, new_state.name)
@@ -196,14 +186,14 @@ async def _ping_task_func(self):
 
             await asyncio.sleep(self._ping_timeout)
 
-    def _start_reconnect(self, return_exceptions=False):
+    def _start_reconnect(self, return_exceptions: bool = False):
         if self._state in [ConnectionState.CONNECTING,
                            ConnectionState.RECONNECTING]:
             logger.debug('%s Cannot start reconnect: already reconnecting',
                          self.fingerprint)
             return
 
-        if self._reconnect_task:
+        if self._reconnect_task:  # pragma: nocover
             return
 
         logger.info('%s Started reconnecting', self.fingerprint)
@@ -213,7 +203,9 @@ def _start_reconnect(self, return_exceptions=False):
             self._connect(return_exceptions=return_exceptions)
         )
 
-    def protocol_factory(self, connected_fut, cls=protocol.Protocol):
+    def protocol_factory(self,
+                         connected_fut: asyncio.Future,
+                         cls=protocol.Protocol):
         return cls(host=self._host,
                    port=self._port,
                    username=self._username,
@@ -228,7 +220,14 @@ def protocol_factory(self, connected_fut, cls=protocol.Protocol):
                    on_connection_lost=self.connection_lost,
                    loop=self._loop)
 
-    async def _connect(self, return_exceptions=True):
+    async def _connect(self, return_exceptions: bool = True):
+        if self._loop is None:
+            self._loop = get_running_loop()
+            if hasattr(self._loop, 'create_task'):
+                self.__create_task = self._loop.create_task
+            else:  # pragma: nocover
+                self.__create_task = asyncio.ensure_future
+
         async with self._connect_lock:
             while True:
                 try:
@@ -278,7 +277,7 @@ async def full_connect():
 
                                 await asyncio.wait_for(connected_fut,
                                                        timeout=timeout)
-                            except asyncio.TimeoutError:
+                            except asyncio.TimeoutError:  # pragma: nocover
                                 tr.close()
                                 continue  # try again
                             except Exception:
@@ -297,7 +296,7 @@ async def full_connect():
 
                     self._transport = tr
                     self._protocol = pr
-                    self._db = self._protocol.get_common_db()
+                    self._set_db(self._protocol.get_common_db())
                     self._reconnect_task = None
                     self._normalize_api()
 
@@ -309,7 +308,7 @@ async def full_connect():
                     skip_errors = {
                         ErrorCode.ER_LOADING,
                         ErrorCode.ER_NO_SUCH_SPACE,
-                        ErrorCode.ER_NO_SUCH_INDEX
+                        ErrorCode.ER_NO_SUCH_INDEX_ID
                     }
                     if e.code in skip_errors:
                         # If Tarantool is still loading then reconnect
@@ -379,7 +378,7 @@ async def disconnect(self):
                 self._ping_task.cancel()
                 self._ping_task = None
 
-            self._db = _DbMock()
+            self._clear_db()
             if self._transport:
                 self._disconnect_waiter = _create_future(self._loop)
                 self._transport.close()
@@ -421,7 +420,7 @@ def close(self):
         self._transport = None
         self._protocol = None
         self._disconnect_waiter = None
-        self._db = _DbMock()
+        self._clear_db()
         self._set_state(ConnectionState.DISCONNECTED)
 
     async def reconnect(self):
@@ -574,6 +573,15 @@ def schema_id(self) -> Optional[int]:
             return None
         return self._protocol.schema_id
 
+    @property
+    def schema(self) -> Optional[protocol.Schema]:
+        """
+            Current Tarantool schema with all spaces, indexes and fields
+        """
+        if self._protocol is None:  # pragma: nocover
+            return None
+        return self._protocol.schema
+
     @property
     def initial_read_buffer_size(self) -> int:
         """
@@ -587,318 +595,19 @@ async def refetch_schema(self):
         """
         await self._protocol.refetch_schema()
 
-    def ping(self, *, timeout=-1.0) -> _MethodRet:
-        """
-            Ping request coroutine
-
-            :param timeout: Request timeout
-
-            :returns: :class:`asynctnt.Response` instance
-        """
-        return self._db.ping(timeout=timeout)
-
-    def call16(self, func_name, args=None, *,
-               timeout=-1.0, push_subscribe=False) -> _MethodRet:
-        """
-            Call16 request coroutine. It is a call with an old behaviour
-            (return result of a Tarantool procedure is wrapped into a tuple,
-            if needed)
-
-            :param func_name: function name to call
-            :param args: arguments to pass to the function (list object)
-            :param timeout: Request timeout
-            :param push_subscribe: Subscribe to push notifications
-
-            :returns: :class:`asynctnt.Response` instance
-        """
-        return self._db.call16(func_name, args,
-                               timeout=timeout,
-                               push_subscribe=push_subscribe)
-
-    def call(self, func_name, args=None, *,
-             timeout=-1.0, push_subscribe=False) -> _MethodRet:
-        """
-            Call request coroutine. It is a call with a new behaviour
-            (return result of a Tarantool procedure is not wrapped into
-            an extra tuple). If you're connecting to Tarantool with
-            version < 1.7, then this call method acts like a call16 method
-
-            Examples:
-
-            .. code-block:: pycon
-
-                # tarantool function:
-                # function f(...)
-                #     return ...
-                # end
-
-                >>> await conn.call('f')
-                <Response sync=3 rowcount=0 data=[]>
-
-                >>> await conn.call('f', [20, 42])
-                <Response sync=3 rowcount=2 data=[20, 42]>
-
-            :param func_name: function name to call
-            :param args: arguments to pass to the function (list object)
-            :param timeout: Request timeout
-            :param push_subscribe: Subscribe to push notifications
-
-            :returns: :class:`asynctnt.Response` instance
-        """
-        return self._db.call(func_name, args,
-                             timeout=timeout, push_subscribe=push_subscribe)
-
-    def eval(self, expression, args=None, *,
-             timeout=-1.0, push_subscribe=False) -> _MethodRet:
-        """
-            Eval request coroutine.
-
-            Examples:
-
-            .. code-block:: pycon
-
-                >>> await conn.eval('return 42')
-                <Response sync=3 rowcount=1 data=[42]>
-
-
-                >>> await conn.eval('return box.info.version')
-                <Response sync=3 rowcount=1 data=['2.1.1-7-gd381a45b6']>
-
-            :param expression: expression to execute
-            :param args: arguments to pass to the function, that will
-                         execute your expression (list object)
-            :param timeout: Request timeout
-            :param push_subscribe: Subscribe to push messages
-
-            :returns: :class:`asynctnt.Response` instance
-        """
-        return self._db.eval(expression, args,
-                             timeout=timeout, push_subscribe=push_subscribe)
-
-    def select(self, space, key=None, **kwargs) -> _MethodRet:
-        """
-            Select request coroutine.
-
-            Examples:
-
-            .. code-block:: pycon
-
-                >>> await conn.select('tester')
-                <Response sync=3 rowcount=2 data=[
-                    <TarantoolTuple id=1 name='one'>,
-                    <TarantoolTuple id=2 name='two'>
-                ]>
-
-                >>> res = await conn.select('_space', ['tester'], index='name')
-                >>> res.data
-                [<TarantoolTuple id=512
-                                 owner=1
-                                 name='tester'
-                                 engine='memtx'
-                                 field_count=0
-                                 flags={}
-                                 format=[
-                                    {'name': 'id', 'type': 'unsigned'},
-                                    {'name': 'name', 'type': 'string'}
-                                 ]>]
-
-
-            :param space: space id or space name.
-            :param key: key to select
-            :param offset: offset to use
-            :param limit: limit to use
-            :param index: index id or name
-            :param iterator: one of the following
-
-                        * iterator id (int number),
-                        * :class:`asynctnt.Iterator` object
-                        * string with an iterator name
-
-            :param timeout: Request timeout
-
-            :returns: :class:`asynctnt.Response` instance
-        """
-        return self._db.select(space, key, **kwargs)
-
-    def insert(self, space, t, *, replace=False, timeout=-1) -> _MethodRet:
-        """
-            Insert request coroutine.
-
-            Examples:
-
-            .. code-block:: pycon
-
-                # Basic usage
-                >>> await conn.insert('tester', [0, 'hello'])
-                <Response sync=3 rowcount=1 data=[
-                    <TarantoolTuple id=0 name='hello'>
-                ]>
-
-                # Using dict as an argument tuple
-                >>> await conn.insert('tester', {
-                ...                     'id': 0
-                ...                     'text': 'hell0'
-                ...                   })
-                <Response sync=3 rowcount=1 data=[
-                    <TarantoolTuple id=0 name='hello'>
-                ]>
-
-            :param space: space id or space name.
-            :param t: tuple to insert (list object)
-            :param replace: performs replace request instead of insert
-            :param timeout: Request timeout
-
-            :returns: :class:`asynctnt.Response` instance
-        """
-        return self._db.insert(space, t,
-                               replace=replace,
-                               timeout=timeout)
-
-    def replace(self, space, t, *, timeout=-1.0) -> _MethodRet:
-        """
-            Replace request coroutine. Same as insert, but replace.
-
-            :param space: space id or space name.
-            :param t: tuple to insert (list object)
-            :param timeout: Request timeout
-
-            :returns: :class:`asynctnt.Response` instance
-        """
-        return self._db.replace(space, t, timeout=timeout)
-
-    def delete(self, space, key, **kwargs) -> _MethodRet:
-        """
-            Delete request coroutine.
-
-            Examples:
-
-            .. code-block:: pycon
-
-                # Assuming tuple [0, 'hello'] is in space tester
-
-                >>> await conn.delete('tester', [0])
-                <Response sync=3 rowcount=1 data=[
-                    <TarantoolTuple id=0 name='hello'>
-                ]>
-
-            :param space: space id or space name.
-            :param key: key to delete
-            :param index: index id or name
-            :param timeout: Request timeout
-
-            :returns: :class:`asynctnt.Response` instance
-        """
-        return self._db.delete(space, key, **kwargs)
-
-    def update(self, space, key, operations, **kwargs) -> _MethodRet:
-        """
-            Update request coroutine.
-
-            Examples:
-
-            .. code-block:: pycon
-
-                # Assuming tuple [0, 'hello'] is in space tester
-
-                >>> await conn.update('tester', [0], [ ['=', 1, 'hi!'] ])
-                <Response sync=3 rowcount=1 data=[
-                    <TarantoolTuple id=0 name='hi!'>
-                ]>
-
-                # you can use fields names as well
-                >>> res = await conn.update('tester', [0],
-                ...                         [ ['=', 'text', 'hola'] ])
-                <Response sync=3 rowcount=1 data=[
-                    <TarantoolTuple id=0 name='hola'>
-                ]>
-
-            :param space: space id or space name.
-            :param key: key to update
-            :param operations:
-                    Operations list of the following format:
-                    [ [op_type, field_no, ...], ... ]. Please refer to
-                    https://tarantool.org/doc/book/box/box_space.html?highlight=update#lua-function.space_object.update
-                    You can use field numbers as well as their names in space
-                    format as a field_no (if only fetch_schema is True).
-                    If field is unknown then TarantoolSchemaError is raised.
-            :param index: index id or name
-            :param timeout: Request timeout
-
-            :returns: :class:`asynctnt.Response` instance
-        """
-        return self._db.update(space, key, operations, **kwargs)
-
-    def upsert(self, space, t, operations, **kwargs) -> _MethodRet:
-        """
-            Update request coroutine. Performs either insert or update
-            (depending of either tuple exists or not)
-
-            Examples:
-
-            .. code-block:: pycon
-
-                # upsert does not return anything
-                >>> await conn.upsert('tester', [0, 'hello'],
-                ...                   [ ['=', 1, 'hi!'] ])
-                <Response sync=3 rowcount=0 data=[]>
-
-            :param space: space id or space name.
-            :param t: tuple to insert if it's not in space
-            :param operations:
-                    Operations list to use for update if tuple is already in
-                    space. It has the same format as in update requets:
-                    [ [op_type, field_no, ...], ... ]. Please refer to
-                    https://tarantool.org/doc/book/box/box_space.html?highlight=update#lua-function.space_object.update
-                    You can use field numbers as well as their names in space
-                    format as a field_no (if only fetch_schema is True).
-                    If field is unknown then TarantoolSchemaError is raised.
-            :param timeout: Request timeout
-
-            :returns: :class:`asynctnt.Response` instance
-        """
-        return self._db.upsert(space, t, operations, **kwargs)
-
-    def sql(self, query, args=None, *,
-            parse_metadata=True, timeout=-1.0) -> _MethodRet:
-        """
-            Executes an SQL statement (only for Tarantool > 2)
-
-            Examples:
-
-            .. code-block:: pycon
-
-                >>> await conn.sql("select 1 as a, 2 as b")
-                <Response sync=3 rowcount=1 data=[<TarantoolTuple A=1 B=2>]>
-
-                >>> await conn.sql("select * from sql_space")
-                <Response sync=3 rowcount=2 data=[
-                    <TarantoolTuple ID=1 NAME='James Bond'>,
-                    <TarantoolTuple ID=2 NAME='Ethan Hunt'>
-                ]>
-
-                >>> await conn.sql("select * from sql_space",
-                ...                parse_metadata=False)
-                <Response sync=3 rowcount=2 data=[
-                    <TarantoolTuple 0=1 1='James Bond'>,
-                    <TarantoolTuple 0=2 1='Ethan Hunt'>
-                ]>
-
-            :param query: SQL query
-            :param args: Query arguments
-            :param parse_metadata: Set to False to disable response's metadata
-                                   parsing for better performance
-            :param timeout: Request timeout
-
-            :returns: :class:`asynctnt.Response` instance
-        """
-        return self._db.sql(query, args,
-                            parse_metadata=parse_metadata,
-                            timeout=timeout)
-
     def _normalize_api(self):
         if (1, 6) <= self.version < (1, 7):  # pragma: nocover
+            Api.call = Api.call16
             Connection.call = Connection.call16
 
+        if self.version < (2, 10):  # pragma: nocover
+            def stream_stub(_):
+                raise TarantoolError(
+                    "streams are available only in Tarantool 2.10+"
+                )
+
+            Connection.stream = stream_stub
+
     def __repr__(self):
         return "<asynctnt.Connection host={} port={} state={}>".format(
             self.host,
@@ -906,6 +615,15 @@ def __repr__(self):
             repr(self.state)
         )
 
+    def stream(self) -> Stream:
+        """
+            Create new stream suitable for interactive transactions
+        """
+        stream = Stream()
+        db = self._protocol.create_db(True)
+        stream._set_db(db)
+        return stream
+
 
 def _create_future(loop):
     try:
@@ -914,11 +632,6 @@ def _create_future(loop):
         return asyncio.Future(loop=loop)
 
 
-class _DbMock:
-    def __getattr__(self, item):
-        raise TarantoolNotConnectedError('Tarantool is not connected')
-
-
 async def connect(**kwargs) -> Connection:
     """
         connect shorthand. See :class:`asynctnt.Connection` for kwargs details
diff --git a/asynctnt/exceptions.py b/asynctnt/exceptions.py
index 11fa7e5..00fa09a 100644
--- a/asynctnt/exceptions.py
+++ b/asynctnt/exceptions.py
@@ -1,4 +1,7 @@
 import enum
+from typing import Optional
+
+from asynctnt.iproto.protocol import IProtoError
 
 
 class TarantoolError(Exception):
@@ -19,10 +22,14 @@ class TarantoolDatabaseError(TarantoolError):
     """
         Exception is raised when Tarantool responds with code != 0
     """
-    def __init__(self, code, message):
-        super(TarantoolDatabaseError, self).__init__(code, message)
+    def __init__(self,
+                 code: int,
+                 message: str,
+                 error: Optional[IProtoError]):
+        super(TarantoolDatabaseError, self).__init__(message)
         self.code = code
         self.message = message
+        self.error = error
 
 
 class TarantoolNetworkError(TarantoolError):
@@ -64,22 +71,22 @@ class ErrorCode(enum.IntEnum):
     ER_PROC_RET = 21
     ER_TUPLE_NOT_ARRAY = 22
     ER_FIELD_TYPE = 23
-    ER_FIELD_TYPE_MISMATCH = 24
-    ER_SPLICE = 25
-    ER_ARG_TYPE = 26
-    ER_TUPLE_IS_TOO_LONG = 27
+    ER_INDEX_PART_TYPE_MISMATCH = 24
+    ER_UPDATE_SPLICE = 25
+    ER_UPDATE_ARG_TYPE = 26
+    ER_FORMAT_MISMATCH_INDEX_PART = 27
     ER_UNKNOWN_UPDATE_OP = 28
     ER_UPDATE_FIELD = 29
-    ER_FIBER_STACK = 30
+    ER_FUNCTION_TX_ACTIVE = 30
     ER_KEY_PART_COUNT = 31
     ER_PROC_LUA = 32
     ER_NO_SUCH_PROC = 33
     ER_NO_SUCH_TRIGGER = 34
-    ER_NO_SUCH_INDEX = 35
+    ER_NO_SUCH_INDEX_ID = 35
     ER_NO_SUCH_SPACE = 36
-    ER_NO_SUCH_FIELD = 37
+    ER_NO_SUCH_FIELD_NO = 37
     ER_EXACT_FIELD_COUNT = 38
-    ER_INDEX_FIELD_COUNT = 39
+    ER_FIELD_MISSING = 39
     ER_WAL_IO = 40
     ER_MORE_THAN_ONE_TUPLE = 41
     ER_ACCESS_DENIED = 42
@@ -93,21 +100,21 @@ class ErrorCode(enum.IntEnum):
     ER_CREATE_FUNCTION = 50
     ER_NO_SUCH_FUNCTION = 51
     ER_FUNCTION_EXISTS = 52
-    ER_FUNCTION_ACCESS_DENIED = 53
-    ER_FUNCTION_MAX = 54
-    ER_SPACE_ACCESS_DENIED = 55
+    ER_BEFORE_REPLACE_RET = 53
+    ER_MULTISTATEMENT_TRANSACTION = 54
+    ER_TRIGGER_EXISTS = 55
     ER_USER_MAX = 56
     ER_NO_SUCH_ENGINE = 57
     ER_RELOAD_CFG = 58
     ER_CFG = 59
-    ER_VINYL = 60
-    ER_LOCAL_SERVER_IS_NOT_ACTIVE = 61
-    ER_UNKNOWN_SERVER = 62
-    ER_CLUSTER_ID_MISMATCH = 63
+    ER_SAVEPOINT_EMPTY_TX = 60
+    ER_NO_SUCH_SAVEPOINT = 61
+    ER_UNKNOWN_REPLICA = 62
+    ER_REPLICASET_UUID_MISMATCH = 63
     ER_INVALID_UUID = 64
-    ER_CLUSTER_ID_IS_RO = 65
-    ER_SERVER_ID_MISMATCH = 66
-    ER_SERVER_ID_IS_RESERVED = 67
+    ER_REPLICASET_UUID_IS_RO = 65
+    ER_INSTANCE_UUID_MISMATCH = 66
+    ER_REPLICA_ID_IS_RESERVED = 67
     ER_INVALID_ORDER = 68
     ER_MISSING_REQUEST_FIELD = 69
     ER_IDENTIFIER = 70
@@ -120,13 +127,13 @@ class ErrorCode(enum.IntEnum):
     ER_NO_CONNECTION = 77
     ER_TIMEOUT = 78
     ER_ACTIVE_TRANSACTION = 79
-    ER_NO_ACTIVE_TRANSACTION = 80
+    ER_CURSOR_NO_TRANSACTION = 80
     ER_CROSS_ENGINE_TRANSACTION = 81
     ER_NO_SUCH_ROLE = 82
     ER_ROLE_EXISTS = 83
     ER_CREATE_ROLE = 84
     ER_INDEX_EXISTS = 85
-    ER_TUPLE_REF_OVERFLOW = 86
+    ER_SESSION_CLOSED = 86
     ER_ROLE_LOOP = 87
     ER_GRANT = 88
     ER_PRIV_GRANTED = 89
@@ -138,7 +145,7 @@ class ErrorCode(enum.IntEnum):
     ER_UPDATE_INTEGER_OVERFLOW = 95
     ER_GUEST_USER_PASSWORD = 96
     ER_TRANSACTION_CONFLICT = 97
-    ER_UNSUPPORTED_ROLE_PRIV = 98
+    ER_UNSUPPORTED_PRIV = 98
     ER_LOAD_FUNCTION = 99
     ER_FUNCTION_LANGUAGE = 100
     ER_RTREE_RECT = 101
@@ -150,13 +157,138 @@ class ErrorCode(enum.IntEnum):
     ER_WRONG_INDEX_PARTS = 107
     ER_WRONG_INDEX_OPTIONS = 108
     ER_WRONG_SCHEMA_VERSION = 109
-    ER_SLAB_ALLOC_MAX = 110
+    ER_MEMTX_MAX_TUPLE_SIZE = 110
     ER_WRONG_SPACE_OPTIONS = 111
     ER_UNSUPPORTED_INDEX_FEATURE = 112
     ER_VIEW_IS_RO = 113
-    ER_SERVER_UUID_MISMATCH = 114
+    ER_NO_TRANSACTION = 114
     ER_SYSTEM = 115
     ER_LOADING = 116
     ER_CONNECTION_TO_SELF = 117
     ER_KEY_PART_IS_TOO_LONG = 118
     ER_COMPRESSION = 119
+    ER_CHECKPOINT_IN_PROGRESS = 120
+    ER_SUB_STMT_MAX = 121
+    ER_COMMIT_IN_SUB_STMT = 122
+    ER_ROLLBACK_IN_SUB_STMT = 123
+    ER_DECOMPRESSION = 124
+    ER_INVALID_XLOG_TYPE = 125
+    ER_ALREADY_RUNNING = 126
+    ER_INDEX_FIELD_COUNT_LIMIT = 127
+    ER_LOCAL_INSTANCE_ID_IS_READ_ONLY = 128
+    ER_BACKUP_IN_PROGRESS = 129
+    ER_READ_VIEW_ABORTED = 130
+    ER_INVALID_INDEX_FILE = 131
+    ER_INVALID_RUN_FILE = 132
+    ER_INVALID_VYLOG_FILE = 133
+    ER_CASCADE_ROLLBACK = 134
+    ER_VY_QUOTA_TIMEOUT = 135
+    ER_PARTIAL_KEY = 136
+    ER_TRUNCATE_SYSTEM_SPACE = 137
+    ER_LOAD_MODULE = 138
+    ER_VINYL_MAX_TUPLE_SIZE = 139
+    ER_WRONG_DD_VERSION = 140
+    ER_WRONG_SPACE_FORMAT = 141
+    ER_CREATE_SEQUENCE = 142
+    ER_ALTER_SEQUENCE = 143
+    ER_DROP_SEQUENCE = 144
+    ER_NO_SUCH_SEQUENCE = 145
+    ER_SEQUENCE_EXISTS = 146
+    ER_SEQUENCE_OVERFLOW = 147
+    ER_NO_SUCH_INDEX_NAME = 148
+    ER_SPACE_FIELD_IS_DUPLICATE = 149
+    ER_CANT_CREATE_COLLATION = 150
+    ER_WRONG_COLLATION_OPTIONS = 151
+    ER_NULLABLE_PRIMARY = 152
+    ER_NO_SUCH_FIELD_NAME_IN_SPACE = 153
+    ER_TRANSACTION_YIELD = 154
+    ER_NO_SUCH_GROUP = 155
+    ER_SQL_BIND_VALUE = 156
+    ER_SQL_BIND_TYPE = 157
+    ER_SQL_BIND_PARAMETER_MAX = 158
+    ER_SQL_EXECUTE = 159
+    ER_UPDATE_DECIMAL_OVERFLOW = 160
+    ER_SQL_BIND_NOT_FOUND = 161
+    ER_ACTION_MISMATCH = 162
+    ER_VIEW_MISSING_SQL = 163
+    ER_FOREIGN_KEY_CONSTRAINT = 164
+    ER_NO_SUCH_MODULE = 165
+    ER_NO_SUCH_COLLATION = 166
+    ER_CREATE_FK_CONSTRAINT = 167
+    ER_DROP_FK_CONSTRAINT = 168
+    ER_NO_SUCH_CONSTRAINT = 169
+    ER_CONSTRAINT_EXISTS = 170
+    ER_SQL_TYPE_MISMATCH = 171
+    ER_ROWID_OVERFLOW = 172
+    ER_DROP_COLLATION = 173
+    ER_ILLEGAL_COLLATION_MIX = 174
+    ER_SQL_NO_SUCH_PRAGMA = 175
+    ER_SQL_CANT_RESOLVE_FIELD = 176
+    ER_INDEX_EXISTS_IN_SPACE = 177
+    ER_INCONSISTENT_TYPES = 178
+    ER_SQL_SYNTAX_WITH_POS = 179
+    ER_SQL_STACK_OVERFLOW = 180
+    ER_SQL_SELECT_WILDCARD = 181
+    ER_SQL_STATEMENT_EMPTY = 182
+    ER_SQL_KEYWORD_IS_RESERVED = 183
+    ER_SQL_SYNTAX_NEAR_TOKEN = 184
+    ER_SQL_UNKNOWN_TOKEN = 185
+    ER_SQL_PARSER_GENERIC = 186
+    ER_SQL_ANALYZE_ARGUMENT = 187
+    ER_SQL_COLUMN_COUNT_MAX = 188
+    ER_HEX_LITERAL_MAX = 189
+    ER_INT_LITERAL_MAX = 190
+    ER_SQL_PARSER_LIMIT = 191
+    ER_INDEX_DEF_UNSUPPORTED = 192
+    ER_CK_DEF_UNSUPPORTED = 193
+    ER_MULTIKEY_INDEX_MISMATCH = 194
+    ER_CREATE_CK_CONSTRAINT = 195
+    ER_CK_CONSTRAINT_FAILED = 196
+    ER_SQL_COLUMN_COUNT = 197
+    ER_FUNC_INDEX_FUNC = 198
+    ER_FUNC_INDEX_FORMAT = 199
+    ER_FUNC_INDEX_PARTS = 200
+    ER_NO_SUCH_FIELD_NAME = 201
+    ER_FUNC_WRONG_ARG_COUNT = 202
+    ER_BOOTSTRAP_READONLY = 203
+    ER_SQL_FUNC_WRONG_RET_COUNT = 204
+    ER_FUNC_INVALID_RETURN_TYPE = 205
+    ER_SQL_PARSER_GENERIC_WITH_POS = 206
+    ER_REPLICA_NOT_ANON = 207
+    ER_CANNOT_REGISTER = 208
+    ER_SESSION_SETTING_INVALID_VALUE = 209
+    ER_SQL_PREPARE = 210
+    ER_WRONG_QUERY_ID = 211
+    ER_SEQUENCE_NOT_STARTED = 212
+    ER_NO_SUCH_SESSION_SETTING = 213
+    ER_UNCOMMITTED_FOREIGN_SYNC_TXNS = 214
+    ER_SYNC_MASTER_MISMATCH = 215
+    ER_SYNC_QUORUM_TIMEOUT = 216
+    ER_SYNC_ROLLBACK = 217
+    ER_TUPLE_METADATA_IS_TOO_BIG = 218
+    ER_XLOG_GAP = 219
+    ER_TOO_EARLY_SUBSCRIBE = 220
+    ER_SQL_CANT_ADD_AUTOINC = 221
+    ER_QUORUM_WAIT = 222
+    ER_INTERFERING_PROMOTE = 223
+    ER_ELECTION_DISABLED = 224
+    ER_TXN_ROLLBACK = 225
+    ER_NOT_LEADER = 226
+    ER_SYNC_QUEUE_UNCLAIMED = 227
+    ER_SYNC_QUEUE_FOREIGN = 228
+    ER_UNABLE_TO_PROCESS_IN_STREAM = 229
+    ER_UNABLE_TO_PROCESS_OUT_OF_STREAM = 230
+    ER_TRANSACTION_TIMEOUT = 231
+    ER_ACTIVE_TIMER = 232
+    ER_TUPLE_FIELD_COUNT_LIMIT = 233
+    ER_CREATE_CONSTRAINT = 234
+    ER_FIELD_CONSTRAINT_FAILED = 235
+    ER_TUPLE_CONSTRAINT_FAILED = 236
+    ER_CREATE_FOREIGN_KEY = 237
+    ER_FOREIGN_KEY_INTEGRITY = 238
+    ER_FIELD_FOREIGN_KEY_FAILED = 239
+    ER_COMPLEX_FOREIGN_KEY_FAILED = 240
+    ER_WRONG_SPACE_UPGRADE_OPTIONS = 241
+    ER_NO_ELECTION_QUORUM = 242
+    ER_SSL = 243
+    ER_SPLIT_BRAIN = 244
diff --git a/asynctnt/instance.py b/asynctnt/instance.py
index b583537..38f2bbc 100644
--- a/asynctnt/instance.py
+++ b/asynctnt/instance.py
@@ -103,6 +103,7 @@ def __init__(self, *,
                  cleanup=True,
                  initlua_template=None,
                  applua='-- app.lua --',
+                 extra_box_cfg='',
                  timeout=5.,
                  command_to_run='tarantool',
                  command_args=None):
@@ -136,6 +137,8 @@ def __init__(self, *,
                                  _create_initlua_template function)
         :param applua: Any extra lua script (a string)
                        (default = '-- app.lua --')
+        :param extra_box_cfg: Any extra box.cfg params
+                              (default = '')
         :param timeout: Timeout in seconds - how much to wait for tarantool
                         to become active
         :param command_to_run: command exe
@@ -159,6 +162,7 @@ def __init__(self, *,
         self._initlua_template = initlua_template or \
                                  self._create_initlua_template()
         self._applua = applua
+        self._extra_box_cfg = extra_box_cfg
         self._command_to_run = command_to_run
         self._command_args = command_args
 
@@ -249,7 +253,8 @@ def _create_initlua_template(self):
               custom_proc_title = "${custom_proc_title}",
               slab_alloc_arena = ${slab_alloc_arena},
               work_dir = ${work_dir},
-              log_level = ${log_level}
+              log_level = ${log_level},
+              ${extra_box_cfg}
             }
             if check_version({1, 7}, _TARANTOOL) then
                 cfg.replication = ${replication_source}
@@ -295,7 +300,8 @@ def _render_initlua(self):
             'replication_source': replication,
             'work_dir': work_dir,
             'log_level': self._log_level,
-            'applua': self._applua if self._applua else ''
+            'applua': self._applua if self._applua else '',
+            'extra_box_cfg': self._extra_box_cfg,
         }
         return template.substitute(d)
 
@@ -564,15 +570,27 @@ def cleanup(self):
             self._logger_thread.join()
         super().cleanup()
 
+    @staticmethod
+    def _parse_version(version: str) -> Optional[tuple]:
+        m = VERSION_STRING_REGEX.match(version)
+        if m is not None:
+            ver = m.group(1)
+            return tuple(map(int, ver.split('.')))
+        return None
+
     def version(self) -> Optional[tuple]:
         res = self.command("box.info.version")
         if not res:
             return None
-        res = res[0]
-        m = VERSION_STRING_REGEX.match(res)
-        if m is not None:
-            ver = m.group(1)
-            return tuple(map(int, ver.split('.')))
+        return self._parse_version(res[0])
+
+    @property
+    def bin_version(self) -> Optional[tuple]:
+        proc = subprocess.Popen([self._command_to_run, '-V'],
+                                stdout=subprocess.PIPE)
+        output = proc.stdout.read().decode()
+        version_str = output.split('\n')[0].split(' ')[1]
+        return self._parse_version(version_str)
 
     def command(self, cmd, print_greeting=True):
         s = TcpSocket(self._console_host, self._console_port)
@@ -776,3 +794,7 @@ def __init__(self, *,
         args = cmd.split(' ')
         self._command_to_run = args[0]
         self._command_args = args[1:]
+
+    @property
+    def bin_version(self) -> Optional[tuple]:
+        return self._parse_version(self._docker_tag)
diff --git a/asynctnt/iproto/bit.pxd b/asynctnt/iproto/bit.pxd
new file mode 100644
index 0000000..334c059
--- /dev/null
+++ b/asynctnt/iproto/bit.pxd
@@ -0,0 +1,35 @@
+from libc.stdint cimport uint64_t, uint32_t, uint16_t
+from libc.string cimport memcpy
+
+cdef inline uint64_t load_u64(const void * p):
+    cdef:
+        uint64_t res
+
+    res = 0
+    memcpy(&res, p, sizeof(res))
+    return res
+
+cdef inline uint64_t load_u32(const void * p):
+    cdef:
+        uint32_t res
+
+    res = 0
+    memcpy(&res, p, sizeof(res))
+    return res
+
+cdef inline uint64_t load_u16(const void * p):
+    cdef:
+        uint16_t res
+
+    res = 0
+    memcpy(&res, p, sizeof(res))
+    return res
+
+cdef inline void store_u64(void * p, uint64_t v):
+    memcpy(p, &v, sizeof(v))
+
+cdef inline void store_u32(void * p, uint32_t v):
+    memcpy(p, &v, sizeof(v))
+
+cdef inline void store_u16(void * p, uint16_t v):
+    memcpy(p, &v, sizeof(v))
diff --git a/asynctnt/iproto/buffer.pxd b/asynctnt/iproto/buffer.pxd
index dd73bf9..f39af09 100644
--- a/asynctnt/iproto/buffer.pxd
+++ b/asynctnt/iproto/buffer.pxd
@@ -1,6 +1,7 @@
+cimport cython
 from libc.stdint cimport uint32_t, uint64_t, int64_t
 
-
+@cython.final
 cdef class WriteBuffer:
     cdef:
         # Preallocated small buffer
@@ -14,61 +15,37 @@ cdef class WriteBuffer:
 
         bytes _encoding
 
-        ssize_t __op_offset
-        ssize_t __sync_offset
-        ssize_t __schema_id_offset
-
     @staticmethod
-    cdef WriteBuffer new(bytes encoding)
+    cdef WriteBuffer create(bytes encoding)
 
     cdef inline _check_readonly(self)
     cdef inline len(self)
-    cdef void ensure_allocated(self, ssize_t extra_length) except *
+    cdef int ensure_allocated(self, ssize_t extra_length) except -1
     cdef char *_ensure_allocated(self, char *p,
-                                        ssize_t extra_length) except NULL
-    cdef void _reallocate(self, ssize_t new_size) except *
-    cdef void write_buffer(self, WriteBuffer buf) except *
-    cdef void write_header(self, uint64_t sync,
-                           tarantool.iproto_type op,
-                           int64_t schema_id) except *
-    cdef void change_schema_id(self, int64_t new_schema_id)
+                                 ssize_t extra_length) except NULL
+    cdef int _reallocate(self, ssize_t new_size) except -1
+    cdef int write_buffer(self, WriteBuffer buf) except -1
+    cdef int write_header(self, uint64_t sync,
+                          tarantool.iproto_type op,
+                          int64_t schema_id,
+                          uint64_t stream_id) except -1
     cdef void write_length(self)
 
-    cdef char *_encode_nil(self, char *p) except NULL
-    cdef char *_encode_bool(self, char *p, bint value) except NULL
-    cdef char *_encode_double(self, char *p, double value) except NULL
-    cdef char *_encode_uint(self, char *p, uint64_t value) except NULL
-    cdef char *_encode_int(self, char *p, int64_t value) except NULL
-    cdef char *_encode_str(self, char *p,
-                           const char *str, uint32_t len) except NULL
-    cdef char *_encode_bin(self, char *p,
-                           const char *data, uint32_t len) except NULL
-    cdef char *_encode_array(self, char *p, uint32_t len) except NULL
-    cdef char *_encode_map(self, char *p, uint32_t len) except NULL
-    cdef char *_encode_list(self, char *p, list arr) except NULL
-    cdef char *_encode_tuple(self, char *p, tuple t) except NULL
-    cdef char *_encode_dict(self, char *p, dict d) except NULL
-    cdef char *_encode_key_sequence(self, char *p, t,
-                                    TntFields fields=*,
-                                    bint default_none=*) except NULL
-    cdef char *_encode_obj(self, char *p, object o) except NULL
-    cdef char *_encode_update_ops(self, char *p, list operations,
-                                  SchemaSpace space) except NULL
-
-    cdef void encode_request_call(self, str func_name, args) except *
-    cdef void encode_request_eval(self, str expression, args) except *
-    cdef void encode_request_select(self, SchemaSpace space, SchemaIndex index,
-                                    key, uint64_t offset, uint64_t limit,
-                                    uint32_t iterator) except *
-    cdef void encode_request_insert(self, SchemaSpace space, t) except *
-    cdef void encode_request_delete(self, SchemaSpace space, SchemaIndex index,
-                                    key) except *
-    cdef void encode_request_update(self, SchemaSpace space, SchemaIndex index,
-                                    key_tuple, list operations,
-                                    bint is_upsert=*) except *
-    cdef void encode_request_upsert(self, SchemaSpace space,
-                                    t, list operations) except *
-    cdef void encode_request_sql(self, str query, args) except *
-    cdef void encode_request_auth(self,
-                                  bytes username,
-                                  bytes scramble) except *
+    cdef char *mp_encode_nil(self, char *p) except NULL
+    cdef char *mp_encode_bool(self, char *p, bint value) except NULL
+    cdef char *mp_encode_double(self, char *p, double value) except NULL
+    cdef char *mp_encode_uint(self, char *p, uint64_t value) except NULL
+    cdef char *mp_encode_int(self, char *p, int64_t value) except NULL
+    cdef char *mp_encode_str(self, char *p,
+                             const char *str, uint32_t len) except NULL
+    cdef char *mp_encode_bin(self, char *p,
+                             const char *data, uint32_t len) except NULL
+    cdef char *mp_encode_decimal(self, char *p, object value) except NULL
+    cdef char *mp_encode_uuid(self, char *p, object value) except NULL
+    cdef char *mp_encode_datetime(self, char *p, object value) except NULL
+    cdef char *mp_encode_array(self, char *p, uint32_t len) except NULL
+    cdef char *mp_encode_map(self, char *p, uint32_t len) except NULL
+    cdef char *mp_encode_list(self, char *p, list arr) except NULL
+    cdef char *mp_encode_tuple(self, char *p, tuple t) except NULL
+    cdef char *mp_encode_dict(self, char *p, dict d) except NULL
+    cdef char *mp_encode_obj(self, char *p, object o) except NULL
diff --git a/asynctnt/iproto/buffer.pyx b/asynctnt/iproto/buffer.pyx
index 5fb4fee..c859f47 100644
--- a/asynctnt/iproto/buffer.pyx
+++ b/asynctnt/iproto/buffer.pyx
@@ -8,10 +8,14 @@ cimport cpython.unicode
 
 from cpython.mem cimport PyMem_Malloc, PyMem_Realloc, PyMem_Free
 from cpython.ref cimport PyObject
+from cpython.datetime cimport datetime
 
 from libc.string cimport memcpy
-from libc.stdint cimport uint32_t, uint64_t, int64_t
+from libc.stdint cimport uint32_t, uint64_t, int64_t, uint8_t
+from libc.stdio cimport printf
 
+from decimal import Decimal  # pragma: nocover
+from uuid import UUID  # pragma: nocover
 
 # noinspection PyUnresolvedReferences
 # noinspection PyAttributeOutsideInit
@@ -25,9 +29,6 @@ cdef class WriteBuffer:
         self._size = _BUFFER_INITIAL_SIZE
         self._length = 0
         self._encoding = None
-        self.__op_offset = -1
-        self.__sync_offset = -1
-        self.__schema_id_offset = -1
 
     def __dealloc__(self):
         if self._buf is not NULL and not self._smallbuf_inuse:
@@ -35,7 +36,7 @@ cdef class WriteBuffer:
             self._buf = NULL
             self._size = 0
 
-        if self._view_count:
+        if self._view_count:  # pragma: nocover
             raise RuntimeError(
                 'Deallocating buffer with attached memoryviews')
 
@@ -51,25 +52,25 @@ cdef class WriteBuffer:
     def __releasebuffer__(self, Py_buffer *buffer):
         self._view_count -= 1
 
-    def hex(self):
-        return ":".join("{:02x}".format(ord(<bytes>(self._buf[i])))
+    def hex(self):  # pragma: nocover
+        return ":".join("{:02x}".format(ord(<bytes> (self._buf[i])))
                         for i in range(self._length))
 
     @staticmethod
-    cdef WriteBuffer new(bytes encoding):
+    cdef WriteBuffer create(bytes encoding):
         cdef WriteBuffer buf
         buf = WriteBuffer.__new__(WriteBuffer)
         buf._encoding = encoding
         return buf
 
-    cdef inline _check_readonly(self):
+    cdef inline _check_readonly(self):  # pragma: nocover
         if self._view_count:
             raise BufferError('the buffer is in read-only mode')
 
     cdef inline len(self):
         return self._length
 
-    cdef void ensure_allocated(self, ssize_t extra_length) except *:
+    cdef int ensure_allocated(self, ssize_t extra_length) except -1:
         cdef ssize_t new_size = extra_length + self._length
 
         if new_size > self._size:
@@ -88,7 +89,7 @@ cdef class WriteBuffer:
             p = &self._buf[used]
         return p
 
-    cdef void _reallocate(self, ssize_t new_size) except *:
+    cdef int _reallocate(self, ssize_t new_size) except -1:
         cdef char *new_buf
 
         if new_size < _BUFFER_MAX_GROW:
@@ -98,18 +99,18 @@ cdef class WriteBuffer:
             new_size += _BUFFER_INITIAL_SIZE
 
         if self._smallbuf_inuse:
-            new_buf = <char*>PyMem_Malloc(sizeof(char) * <size_t>new_size)
-            if new_buf is NULL:
+            new_buf = <char*> PyMem_Malloc(sizeof(char) * <size_t> new_size)
+            if new_buf is NULL:  # pragma: nocover
                 self._buf = NULL
                 self._size = 0
                 self._length = 0
                 raise MemoryError
-            memcpy(new_buf, self._buf, <size_t>self._size)
+            memcpy(new_buf, self._buf, <size_t> self._size)
             self._size = new_size
             self._buf = new_buf
             self._smallbuf_inuse = False
         else:
-            new_buf = <char*>PyMem_Realloc(<void*>self._buf, <size_t>new_size)
+            new_buf = <char*> PyMem_Realloc(<void*> self._buf, <size_t> new_size)
             if new_buf is NULL:
                 PyMem_Free(self._buf)
                 self._buf = NULL
@@ -119,50 +120,47 @@ cdef class WriteBuffer:
             self._buf = new_buf
             self._size = new_size
 
-    cdef void write_buffer(self, WriteBuffer buf) except *:
+    cdef int write_buffer(self, WriteBuffer buf) except -1:
         if not buf._length:
-            return
+            return 0
 
         self.ensure_allocated(buf._length)
         memcpy(self._buf + self._length,
-               <void*>buf._buf,
-               <size_t>buf._length)
+               <void*> buf._buf,
+               <size_t> buf._length)
         self._length += buf._length
 
-    cdef void write_header(self, uint64_t sync,
-                           tarantool.iproto_type op,
-                           int64_t schema_id) except *:
+    cdef int write_header(self, uint64_t sync,
+                          tarantool.iproto_type op,
+                          int64_t schema_id,
+                          uint64_t stream_id) except -1:
         cdef:
             char *begin = NULL
             char *p = NULL
             uint32_t map_size
         self.ensure_allocated(HEADER_CONST_LEN)
 
-        map_size = 2 + <uint32_t>(schema_id > 0)
+        map_size = 2 \
+                    + (<uint32_t> (schema_id > 0)) \
+                    + (<uint32_t> (stream_id > 0))
 
         p = begin = &self._buf[self._length]
         p = mp_encode_map(&p[5], map_size)
         p = mp_encode_uint(p, tarantool.IPROTO_REQUEST_TYPE)
-        self.__op_offset = (p - begin)  # save op position
-        p = mp_encode_uint(p, <uint32_t>op)
+        p = mp_encode_uint(p, <uint32_t> op)
         p = mp_encode_uint(p, tarantool.IPROTO_SYNC)
-        self.__sync_offset = (p - begin)  # save sync position
         p = mp_encode_uint(p, sync)
 
-        if schema_id > 0:
+        if schema_id > 0:  # pragma: nocover  # asynctnt does not send schema_id
             p = mp_encode_uint(p, tarantool.IPROTO_SCHEMA_VERSION)
             p = mp_store_u8(p, 0xce)
-            self.__schema_id_offset = (p - begin)  # save schema_id position
             p = mp_store_u32(p, schema_id)
 
-        self._length += (p - begin)
+        if stream_id > 0:
+            p = mp_encode_uint(p, tarantool.IPROTO_STREAM_ID)
+            p = mp_encode_uint(p, stream_id)
 
-    cdef void change_schema_id(self, int64_t new_schema_id):
-        cdef char *p
-
-        if self.__schema_id_offset > 0 and new_schema_id > 0:
-            p = &self._buf[self.__schema_id_offset]
-            p = mp_store_u32(p, new_schema_id)
+        self._length += (p - begin)
 
     cdef void write_length(self):
         cdef:
@@ -171,110 +169,170 @@ cdef class WriteBuffer:
         p = mp_store_u8(p, 0xce)
         p = mp_store_u32(p, self._length - 5)
 
-    cdef char *_encode_nil(self, char *p) except NULL:
+    cdef char *mp_encode_nil(self, char *p) except NULL:
         cdef char *begin
         p = begin = self._ensure_allocated(p, 1)
         p = mp_encode_nil(p)
         self._length += (p - begin)
         return p
 
-    cdef char *_encode_bool(self, char *p, bint value) except NULL:
+    cdef char *mp_encode_bool(self, char *p, bint value) except NULL:
         cdef char *begin
         p = begin = self._ensure_allocated(p, 1)
         p = mp_encode_bool(p, value)
         self._length += (p - begin)
         return p
 
-    cdef char *_encode_double(self, char *p, double value) except NULL:
+    cdef char *mp_encode_double(self, char *p, double value) except NULL:
         cdef char *begin
         p = begin = self._ensure_allocated(p, mp_sizeof_double(value))
         p = mp_encode_double(p, value)
         self._length += (p - begin)
         return p
 
-    cdef char *_encode_uint(self, char *p, uint64_t value) except NULL:
+    cdef char *mp_encode_uint(self, char *p, uint64_t value) except NULL:
         cdef char *begin
         p = begin = self._ensure_allocated(p, mp_sizeof_uint(value))
         p = mp_encode_uint(p, value)
         self._length += (p - begin)
         return p
 
-    cdef char *_encode_int(self, char *p, int64_t value) except NULL:
+    cdef char *mp_encode_int(self, char *p, int64_t value) except NULL:
         cdef char *begin
         p = begin = self._ensure_allocated(p, mp_sizeof_int(value))
         p = mp_encode_int(p, value)
         self._length += (p - begin)
         return p
 
-    cdef char *_encode_str(self, char *p,
-                           const char *str, uint32_t len) except NULL:
+    cdef char *mp_encode_str(self, char *p,
+                             const char *str, uint32_t len) except NULL:
         cdef char *begin
         p = begin = self._ensure_allocated(p, mp_sizeof_str(len))
         p = mp_encode_str(p, str, len)
         self._length += (p - begin)
         return p
 
-    cdef char *_encode_bin(self, char *p,
-                           const char *data, uint32_t len) except NULL:
+    cdef char *mp_encode_bin(self, char *p,
+                             const char *data, uint32_t len) except NULL:
         cdef char *begin
         p = begin = self._ensure_allocated(p, mp_sizeof_bin(len))
         p = mp_encode_bin(p, data, len)
         self._length += (p - begin)
         return p
 
-    cdef char *_encode_array(self, char *p, uint32_t len) except NULL:
+    cdef char *mp_encode_decimal(self, char *p, object value) except NULL:
+        cdef:
+            char *begin
+            uint8_t sign
+            tuple digits
+            uint32_t digits_count
+            int exponent
+            uint32_t length
+
+        decimal_tuple = value.as_tuple()
+        sign = <uint8_t> decimal_tuple.sign
+        digits = <tuple> decimal_tuple.digits
+        exponent = <int> decimal_tuple.exponent
+
+        digits_count = <uint32_t> len(digits)
+        length = decimal_len(exponent, digits_count)
+
+        p = begin = self._ensure_allocated(p, mp_sizeof_ext(length))
+
+        # encode header
+        p = mp_encode_extl(p, tarantool.MP_DECIMAL, length)
+
+        # encode decimal
+        p = decimal_encode(p, digits_count, sign, digits, exponent)
+
+        self._length += (p - begin)
+        return p
+
+    cdef char *mp_encode_uuid(self, char *p, object value) except NULL:
+        cdef:
+            char *begin
+            char *data_p
+
+        p = begin = self._ensure_allocated(p, mp_sizeof_ext(16))
+        data_p = cpython.bytes.PyBytes_AS_STRING(<bytes> value.bytes)
+
+        # encode header
+        p = mp_encode_ext(p, tarantool.MP_UUID, data_p, 16)  # uuid is exactly 16 bytes
+
+        self._length += (p - begin)
+        return p
+
+    cdef char *mp_encode_datetime(self, char *p, object value) except NULL:
+        cdef:
+            char *begin
+            uint32_t length
+            datetime pydt
+            IProtoDateTime dt
+
+        pydt = <datetime> value
+        datetime_zero(&dt)
+        datetime_from_py(pydt, &dt)
+
+        length = datetime_len(&dt)
+        p = begin = self._ensure_allocated(p, mp_sizeof_ext(length))
+        p = mp_encode_extl(p, tarantool.MP_DATETIME, length)
+        p = datetime_encode(p, &dt)
+        self._length += (p - begin)
+        return p
+
+    cdef char *mp_encode_array(self, char *p, uint32_t len) except NULL:
         cdef char *begin
         p = begin = self._ensure_allocated(p, mp_sizeof_array(len))
         p = mp_encode_array(p, len)
         self._length += (p - begin)
         return p
 
-    cdef char *_encode_map(self, char *p, uint32_t len) except NULL:
+    cdef char *mp_encode_map(self, char *p, uint32_t len) except NULL:
         cdef char *begin
         p = begin = self._ensure_allocated(p, mp_sizeof_map(len))
         p = mp_encode_map(p, len)
         self._length += (p - begin)
         return p
 
-    cdef char *_encode_list(self, char *p, list arr) except NULL:
+    cdef char *mp_encode_list(self, char *p, list arr) except NULL:
         cdef:
             uint32_t arr_len
             PyObject *item_ptr
             object item
 
         if arr is not None:
-            arr_len = <uint32_t>cpython.list.PyList_GET_SIZE(arr)
-        else:
+            arr_len = <uint32_t> cpython.list.PyList_GET_SIZE(arr)
+        else:  # pragma: nocover
             arr_len = 0
-        p = self._encode_array(p, arr_len)
+        p = self.mp_encode_array(p, arr_len)
 
         if arr_len > 0:
             for i in range(arr_len):
                 item_ptr = cpython.list.PyList_GET_ITEM(arr, i)
-                item = <object>item_ptr
-                p = self._encode_obj(p, item)
+                item = <object> item_ptr
+                p = self.mp_encode_obj(p, item)
         return p
 
-    cdef char *_encode_tuple(self, char *p, tuple t) except NULL:
+    cdef char *mp_encode_tuple(self, char *p, tuple t) except NULL:
         cdef:
             uint32_t t_len
             PyObject *item_ptr
             object item
 
         if t is not None:
-            t_len = <uint32_t>cpython.tuple.PyTuple_GET_SIZE(t)
-        else:
+            t_len = <uint32_t> cpython.tuple.PyTuple_GET_SIZE(t)
+        else:  # pragma: nocover
             t_len = 0
-        p = self._encode_array(p, t_len)
+        p = self.mp_encode_array(p, t_len)
 
         if t_len > 0:
             for i in range(t_len):
                 item_ptr = cpython.tuple.PyTuple_GET_ITEM(t, i)
-                item = <object>item_ptr
-                p = self._encode_obj(p, item)
+                item = <object> item_ptr
+                p = self.mp_encode_obj(p, item)
         return p
 
-    cdef char *_encode_dict(self, char *p, dict d) except NULL:
+    cdef char *mp_encode_dict(self, char *p, dict d) except NULL:
         cdef:
             uint32_t d_len
             PyObject *pkey
@@ -283,60 +341,40 @@ cdef class WriteBuffer:
             Py_ssize_t pos
 
         if d is not None:
-            d_len = <uint32_t>cpython.dict.PyDict_Size(d)
-        else:
+            d_len = <uint32_t> cpython.dict.PyDict_Size(d)
+        else:  # pragma: nocover
             d_len = 0
-        p = self._encode_map(p, d_len)
+        p = self.mp_encode_map(p, d_len)
 
         pos = 0
         while cpython.dict.PyDict_Next(d, &pos, &pkey, &pvalue):
-            key = <object>pkey
-            value = <object>pvalue
-            p = self._encode_obj(p, key)
-            p = self._encode_obj(p, value)
+            key = <object> pkey
+            value = <object> pvalue
+            p = self.mp_encode_obj(p, key)
+            p = self.mp_encode_obj(p, value)
 
         return p
 
-    cdef char *_encode_key_sequence(self, char *p, t,
-                                    TntFields fields=None,
-                                    bint default_none=False) except NULL:
-        if isinstance(t, list) or t is None:
-            return self._encode_list(p, <list>t)
-        elif isinstance(t, tuple):
-            return self._encode_tuple(p, <tuple>t)
-        elif isinstance(t, dict) and fields is not None:
-            return self._encode_list(
-                p, dict_to_list_fields(<dict>t, fields, default_none)
-            )
-        else:
-            if fields is not None:
-                msg = 'sequence must be either list, tuple or dict'
-            else:
-                msg = 'sequence must be either list or tuple'
-            raise TypeError(
-                '{}, got: {}'.format(msg, type(t))
-            )
-
-    cdef char *_encode_obj(self, char *p, object o) except NULL:
+    cdef char *mp_encode_obj(self, char *p, object o) except NULL:
         cdef:
             bytes o_string_temp
             char *o_string_str
             ssize_t o_string_len
 
         if o is None:
-            return self._encode_nil(p)
-
-        elif isinstance(o, bool):
-            return self._encode_bool(p, <bint>o)
+            return self.mp_encode_nil(p)
 
         elif isinstance(o, float):
-            return self._encode_double(p, <double>o)
+            return self.mp_encode_double(p, <double> o)
+
+        elif isinstance(o, bool):
+            return self.mp_encode_bool(p, <bint> o)
 
         elif isinstance(o, int):
             if o >= 0:
-                return self._encode_uint(p, <uint64_t>o)
+                return self.mp_encode_uint(p, <uint64_t> o)
             else:
-                return self._encode_int(p, <int64_t>o)
+                return self.mp_encode_int(p, <int64_t> o)
 
         elif isinstance(o, bytes):
             o_string_str = NULL
@@ -344,7 +382,7 @@ cdef class WriteBuffer:
             cpython.bytes.PyBytes_AsStringAndSize(o,
                                                   &o_string_str,
                                                   &o_string_len)
-            return self._encode_bin(p, o_string_str, <uint32_t>o_string_len)
+            return self.mp_encode_bin(p, o_string_str, <uint32_t> o_string_len)
 
         elif isinstance(o, str):
             o_string_temp = encode_unicode_string(o, self._encoding)
@@ -354,541 +392,27 @@ cdef class WriteBuffer:
                                                   &o_string_str,
                                                   &o_string_len)
 
-            p = self._encode_str(p, o_string_str, <uint32_t>o_string_len)
+            p = self.mp_encode_str(p, o_string_str, <uint32_t> o_string_len)
             return p
 
         elif isinstance(o, list):
-            return self._encode_list(p, <list>o)
+            return self.mp_encode_list(p, <list> o)
 
         elif isinstance(o, tuple):
-            return self._encode_tuple(p, <tuple>o)
+            return self.mp_encode_tuple(p, <tuple> o)
 
         elif isinstance(o, dict):
-            return self._encode_dict(p, <dict>o)
-
-        else:
-            raise TypeError(
-                'Type `{}` is not supported for encoding'.format(type(o)))
-
-    cdef char *_encode_update_ops(self, char *p, list operations,
-                                  SchemaSpace space) except NULL:
-        cdef:
-            char *begin
-            uint32_t ops_len, op_len
-            bytes str_temp
-            char *str_c
-            ssize_t str_len
-
-            char *op_str_c
-            ssize_t op_str_len
-            char op
-
-            uint32_t extra_length
-
-            uint64_t field_no
-            object field_no_obj
-
-            uint32_t splice_position, splice_offset
-
-        begin = NULL
-
-        if operations is not None:
-            ops_len = <uint32_t>cpython.list.PyList_GET_SIZE(operations)
-        else:
-            ops_len = 0
-
-        p = self._encode_array(p, ops_len)
-        if ops_len == 0:
-            return p
-
-        for operation in operations:
-            if not isinstance(operation, (list, tuple)):
-                raise TypeError(
-                    'Single operation must be a tuple or list')
-
-            op_len = <uint32_t>cpython.list.PyList_GET_SIZE(operation)
-            if op_len < 3:
-                raise IndexError(
-                    'Operation length must be at least 3')
-
-            op_type_str = operation[0]
-            if isinstance(op_type_str, str):
-                str_temp = encode_unicode_string(op_type_str, self._encoding)
-            elif isinstance(op_type_str, bytes):
-                str_temp = <bytes>op_type_str
-            else:
-                raise TypeError(
-                    'Operation type must of a str or bytes type')
-
-            field_no_obj = operation[1]
-            if isinstance(field_no_obj, int):
-                field_no = <uint64_t>field_no_obj
-            elif isinstance(field_no_obj, str):
-                if space.fields is not None:
-                    field_no = <uint64_t>space.fields.id_by_name(field_no_obj)
-                else:
-                    raise TypeError(
-                        'Operation field_no must be int as there is '
-                        'no format declaration in space {}'.format(space.sid))
-            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]
-
-            if op == tarantool.IPROTO_OP_ADD \
-                    or op == tarantool.IPROTO_OP_SUB \
-                    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 = self._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)
-                self._length += (p - begin)
-                p = self._encode_obj(p, op_argument)
-            elif 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)
-                extra_length = 1 + 2 + mp_sizeof_uint(field_no)
-                p = begin = self._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)
-                self._length += (p - begin)
-                p = self._encode_obj(p, op_argument)
-
-            elif op == tarantool.IPROTO_OP_SPLICE:
-                if op_len < 5:
-                    raise IndexError(
-                        'Splice operation must have length of 5, '
-                        'but got: {}'.format(op_len)
-                    )
-
-                splice_position_obj = operation[2]
-                splice_offset_obj = operation[3]
-                op_argument = operation[4]
-                if not isinstance(splice_position_obj, int):
-                    raise TypeError('Splice position must be int')
-                if not isinstance(splice_offset_obj, int):
-                    raise TypeError('Splice offset must be int')
-
-                splice_position = <uint32_t>splice_position_obj
-                splice_offset = <uint32_t>splice_offset_obj
-
-                # mp_sizeof_array(5) + mp_sizeof_str(1) + ...
-                extra_length = 1 + 2 \
-                                + mp_sizeof_uint(field_no) \
-                                + mp_sizeof_uint(splice_position) \
-                                + mp_sizeof_uint(splice_offset)
-                p = begin = self._ensure_allocated(p, extra_length)
-
-                p = mp_encode_array(p, 5)
-                p = mp_encode_str(p, op_str_c, 1)
-                p = mp_encode_uint(p, field_no)
-                p = mp_encode_uint(p, splice_position)
-                p = mp_encode_uint(p, splice_offset)
-                self._length += (p - begin)
-                p = self._encode_obj(p, op_argument)
-            else:
-                raise TypeError(
-                    'Unknown update operation type `{}`'.format(op_type_str))
-        return p
-
-    cdef void encode_request_call(self, str func_name, args) except *:
-        cdef:
-            char *begin
-            char *p
-            uint32_t body_map_sz
-            uint32_t max_body_len
-
-            bytes func_name_temp
-            char *func_name_str
-            ssize_t func_name_len
-
-        func_name_str = NULL
-        func_name_len = 0
-
-        func_name_temp = encode_unicode_string(func_name, self._encoding)
-        cpython.bytes.PyBytes_AsStringAndSize(func_name_temp,
-                                              &func_name_str,
-                                              &func_name_len)
-        body_map_sz = 2
-        # Size description:
-        # mp_sizeof_map()
-        # + mp_sizeof_uint(TP_FUNCTION)
-        # + mp_sizeof_str(func_name)
-        # + mp_sizeof_uint(TP_TUPLE)
-        max_body_len = 1 \
-                       + 1 \
-                       + mp_sizeof_str(<uint32_t>func_name_len) \
-                       + 1
-
-        self.ensure_allocated(max_body_len)
-
-        p = begin = &self._buf[self._length]
-        p = mp_encode_map(p, body_map_sz)
-        p = mp_encode_uint(p, tarantool.IPROTO_FUNCTION_NAME)
-        p = mp_encode_str(p, func_name_str, <uint32_t>func_name_len)
-
-        p = mp_encode_uint(p, tarantool.IPROTO_TUPLE)
-        self._length += (p - begin)
-        p = self._encode_key_sequence(p, args)
-
-    cdef void encode_request_eval(self, str expression, args) except *:
-        cdef:
-            char *begin
-            char *p
-            uint32_t body_map_sz
-            uint32_t max_body_len
-
-            bytes expression_temp
-            char *expression_str
-            ssize_t expression_len
-
-        expression_str = NULL
-        expression_len = 0
-
-        expression_temp = encode_unicode_string(expression, self._encoding)
-        cpython.bytes.PyBytes_AsStringAndSize(expression_temp,
-                                              &expression_str,
-                                              &expression_len)
-        body_map_sz = 2
-        # Size description:
-        # mp_sizeof_map()
-        # + mp_sizeof_uint(TP_EXPRESSION)
-        # + mp_sizeof_str(expression)
-        # + mp_sizeof_uint(TP_TUPLE)
-        max_body_len = 1 \
-                       + 1 \
-                       + mp_sizeof_str(<uint32_t>expression_len) \
-                       + 1
-
-        self.ensure_allocated(max_body_len)
-
-        p = begin = &self._buf[self._length]
-        p = mp_encode_map(p, body_map_sz)
-        p = mp_encode_uint(p, tarantool.IPROTO_EXPR)
-        p = mp_encode_str(p, expression_str, <uint32_t>expression_len)
-
-        p = mp_encode_uint(p, tarantool.IPROTO_TUPLE)
-        self._length += (p - begin)
-        p = self._encode_key_sequence(p, args)
-
-    cdef void encode_request_select(self, SchemaSpace space, SchemaIndex index,
-                                    key, uint64_t offset, uint64_t limit,
-                                    uint32_t iterator) except *:
-        cdef:
-            char *begin
-            char *p
-            uint32_t body_map_sz
-            uint32_t max_body_len
-            uint32_t space_id, index_id
-
-        space_id = space.sid
-        index_id = index.iid
-
-        body_map_sz = 3 \
-                      + <uint32_t>(index_id > 0) \
-                      + <uint32_t>(offset > 0) \
-                      + <uint32_t>(iterator > 0)
-        # Size description:
-        # mp_sizeof_map(body_map_sz)
-        # + mp_sizeof_uint(TP_SPACE)
-        # + mp_sizeof_uint(space)
-        # + mp_sizeof_uint(TP_LIMIT)
-        # + mp_sizeof_uint(limit)
-        max_body_len = 1 \
-                       + 1 \
-                       + 9 \
-                       + 1 \
-                       + 9
-
-        if index_id > 0:
-            # mp_sizeof_uint(TP_INDEX) + mp_sizeof_uint(index_id)
-            max_body_len += 1 + 9
-        if offset > 0:
-            # mp_sizeof_uint(TP_OFFSET) + mp_sizeof_uint(offset)
-            max_body_len += 1 + 9
-        if iterator > 0:
-            # mp_sizeof_uint(TP_ITERATOR) + mp_sizeof_uint(iterator)
-            max_body_len += 1 + 1
-
-        max_body_len += 1  # mp_sizeof_uint(TP_KEY);
-
-        self.ensure_allocated(max_body_len)
-
-        p = begin = &self._buf[self._length]
-        p = mp_encode_map(p, body_map_sz)
-        p = mp_encode_uint(p, tarantool.IPROTO_SPACE_ID)
-        p = mp_encode_uint(p, space_id)
-        p = mp_encode_uint(p, tarantool.IPROTO_LIMIT)
-        p = mp_encode_uint(p, limit)
-
-        if index_id > 0:
-            p = mp_encode_uint(p, tarantool.IPROTO_INDEX_ID)
-            p = mp_encode_uint(p, index_id)
-        if offset > 0:
-            p = mp_encode_uint(p, tarantool.IPROTO_OFFSET)
-            p = mp_encode_uint(p, offset)
-        if iterator > 0:
-            p = mp_encode_uint(p, tarantool.IPROTO_ITERATOR)
-            p = mp_encode_uint(p, iterator)
-
-        p = mp_encode_uint(p, tarantool.IPROTO_KEY)
-        self._length += (p - begin)
-        p = self._encode_key_sequence(p, key, index.fields, False)
-
-    cdef void encode_request_insert(self, SchemaSpace space, t) except *:
-        cdef:
-            char *begin
-            char *p
-            uint32_t body_map_sz
-            uint32_t max_body_len
-            uint32_t space_id
-
-        space_id = space.sid
-
-        body_map_sz = 2
-        # Size description:
-        # mp_sizeof_map(body_map_sz)
-        # + mp_sizeof_uint(TP_SPACE)
-        # + mp_sizeof_uint(space)
-        # + mp_sizeof_uint(TP_TUPLE)
-        max_body_len = 1 \
-                       + 1 \
-                       + 9 \
-                       + 1
-
-        self.ensure_allocated(max_body_len)
-
-        p = begin = &self._buf[self._length]
-        p = mp_encode_map(p, body_map_sz)
-        p = mp_encode_uint(p, tarantool.IPROTO_SPACE_ID)
-        p = mp_encode_uint(p, space_id)
-
-        p = mp_encode_uint(p, tarantool.IPROTO_TUPLE)
-        self._length += (p - begin)
-        p = self._encode_key_sequence(p, t, space.fields, True)
-
-    cdef void encode_request_delete(self, SchemaSpace space, SchemaIndex index,
-                                    key) except *:
-        cdef:
-            char *p
-            uint32_t body_map_sz
-            uint32_t max_body_len
-            uint32_t space_id, index_id
-
-        space_id = space.sid
-        index_id = index.iid
+            return self.mp_encode_dict(p, <dict> o)
 
-        body_map_sz = 2 \
-                      + <uint32_t>(index_id > 0)
-        # Size description:
-        # mp_sizeof_map(body_map_sz)
-        # + mp_sizeof_uint(TP_SPACE)
-        # + mp_sizeof_uint(space)
-        max_body_len = 1 \
-                       + 1 \
-                       + 9
+        elif isinstance(o, datetime):
+            return self.mp_encode_datetime(p, o)
 
-        if index_id > 0:
-            # mp_sizeof_uint(TP_INDEX) + mp_sizeof_uint(index)
-            max_body_len += 1 + 9
+        elif isinstance(o, Decimal):
+            return self.mp_encode_decimal(p, o)
 
-        max_body_len += 1  # mp_sizeof_uint(TP_KEY);
+        elif isinstance(o, UUID):
+            return self.mp_encode_uuid(p, o)
 
-        self.ensure_allocated(max_body_len)
-
-        p = begin = &self._buf[self._length]
-        p = mp_encode_map(p, body_map_sz)
-        p = mp_encode_uint(p, tarantool.IPROTO_SPACE_ID)
-        p = mp_encode_uint(p, space_id)
-
-        if index_id > 0:
-            p = mp_encode_uint(p, tarantool.IPROTO_INDEX_ID)
-            p = mp_encode_uint(p, index_id)
-
-        p = mp_encode_uint(p, tarantool.IPROTO_KEY)
-        self._length += (p - begin)
-        p = self._encode_key_sequence(p, key, index.fields, False)
-
-    cdef void encode_request_update(self, SchemaSpace space, SchemaIndex index,
-                                    key_tuple, list operations,
-                                    bint is_upsert=False) except *:
-        cdef:
-            char *begin
-            char *p
-            uint32_t body_map_sz
-            uint32_t max_body_len
-            uint32_t space_id, index_id
-            uint32_t key_of_tuple, key_of_operations
-            TntFields fields
-            bint default_fields_none
-
-        space_id = space.sid
-        index_id = index.iid
-
-        if not is_upsert:
-            key_of_tuple = tarantool.IPROTO_KEY
-            key_of_operations = tarantool.IPROTO_TUPLE
-            fields = index.fields
-            default_fields_none = False
         else:
-            key_of_tuple = tarantool.IPROTO_TUPLE
-            key_of_operations = tarantool.IPROTO_OPS
-            fields = space.fields
-            default_fields_none = True
-
-        body_map_sz = 3 + <uint32_t>(index_id > 0)
-        # Size description:
-        # mp_sizeof_map(body_map_sz)
-        # + mp_sizeof_uint(TP_SPACE)
-        # + mp_sizeof_uint(space)
-        max_body_len = 1 \
-                       + 1 \
-                       + 9
-
-        if index_id > 0:
-            # + mp_sizeof_uint(TP_INDEX)
-            # + mp_sizeof_uint(index)
-            max_body_len += 1 + 9
-
-        max_body_len += 1  # + mp_sizeof_uint(TP_KEY)
-        max_body_len += 1  # + mp_sizeof_uint(TP_TUPLE)
-
-        self.ensure_allocated(max_body_len)
-
-        p = begin = &self._buf[self._length]
-        p = mp_encode_map(p, body_map_sz)
-        p = mp_encode_uint(p, tarantool.IPROTO_SPACE_ID)
-        p = mp_encode_uint(p, space_id)
-
-        if index_id > 0:
-            p = mp_encode_uint(p, tarantool.IPROTO_INDEX_ID)
-            p = mp_encode_uint(p, index_id)
-        self._length += (p - begin)
-
-        p = self._encode_uint(p, key_of_tuple)
-        p = self._encode_key_sequence(p, key_tuple, fields,
-                                      default_fields_none)
-
-        p = self._encode_uint(p, key_of_operations)
-        p = self._encode_update_ops(p, operations, space)
-
-    cdef void encode_request_upsert(self, SchemaSpace space, t,
-                                    list operations) except *:
-        self.encode_request_update(space, space.get_index(0),
-                                   t, operations, True)
-
-    cdef void encode_request_sql(self, str query, args) except *:
-        cdef:
-            char *begin
-            char *p
-            uint32_t body_map_sz
-            uint32_t max_body_len
-
-            bytes query_temp
-            char *query_str
-            ssize_t query_len
-
-        query_str = NULL
-        query_len = 0
-
-        query_temp = encode_unicode_string(query, self._encoding)
-        cpython.bytes.PyBytes_AsStringAndSize(query_temp,
-                                              &query_str,
-                                              &query_len)
-        body_map_sz = 2
-        # Size description:
-        # mp_sizeof_map()
-        # + mp_sizeof_uint(TP_SQL_TEXT)
-        # + mp_sizeof_str(query)
-        # + mp_sizeof_uint(TP_SQL_BIND)
-        max_body_len = 1 \
-                       + 1 \
-                       + mp_sizeof_str(<uint32_t>query_len) \
-                       + 1
-
-        self.ensure_allocated(max_body_len)
-
-        p = begin = &self._buf[self._length]
-        p = mp_encode_map(p, body_map_sz)
-        p = mp_encode_uint(p, tarantool.IPROTO_SQL_TEXT)
-        p = mp_encode_str(p, query_str, <uint32_t>query_len)
-
-        p = mp_encode_uint(p, tarantool.IPROTO_SQL_BIND)
-        self._length += (p - begin)
-        # TODO: replace with custom encoder
-        # TODO: need to simultaneously encode ordinal and named params
-        p = self._encode_key_sequence(p, args)
-
-    cdef void encode_request_auth(self,
-                                  bytes username,
-                                  bytes scramble) except *:
-        cdef:
-            char *begin
-            char *p
-            uint32_t body_map_sz
-            uint32_t max_body_len
-
-            char *username_str
-            ssize_t username_len
-
-            char *scramble_str
-            ssize_t scramble_len
-
-        cpython.bytes.PyBytes_AsStringAndSize(username,
-                                              &username_str, &username_len)
-        cpython.bytes.PyBytes_AsStringAndSize(scramble,
-                                              &scramble_str, &scramble_len)
-        body_map_sz = 2
-        # Size description:
-        # mp_sizeof_map()
-        # + mp_sizeof_uint(TP_USERNAME)
-        # + mp_sizeof_str(username_len)
-        # + mp_sizeof_uint(TP_TUPLE)
-        # + mp_sizeof_array(2)
-        # + mp_sizeof_str(9) (chap-sha1)
-        # + mp_sizeof_str(SCRAMBLE_SIZE)
-        max_body_len = 1 \
-                       + 1 \
-                       + mp_sizeof_str(<uint32_t>username_len) \
-                       + 1 \
-                       + 1 \
-                       + 1 + 9 \
-                       + mp_sizeof_str(<uint32_t>scramble_len)
-
-        self.ensure_allocated(max_body_len)
-
-        p = begin = &self._buf[self._length]
-        p = mp_encode_map(p, body_map_sz)
-        p = mp_encode_uint(p, tarantool.IPROTO_USER_NAME)
-        p = mp_encode_str(p, username_str, <uint32_t>username_len)
-
-        p = mp_encode_uint(p, tarantool.IPROTO_TUPLE)
-        p = mp_encode_array(p, 2)
-        p = mp_encode_str(p, "chap-sha1", 9)
-        p = mp_encode_str(p, scramble_str, <uint32_t>scramble_len)
-        self._length += (p - begin)
+            raise TypeError(
+                'Type `{}` is not supported for encoding'.format(type(o)))
diff --git a/asynctnt/iproto/cmsgpuck.pxd b/asynctnt/iproto/cmsgpuck.pxd
index 9fa5b41..68652ef 100644
--- a/asynctnt/iproto/cmsgpuck.pxd
+++ b/asynctnt/iproto/cmsgpuck.pxd
@@ -1,4 +1,4 @@
-from libc.stdint cimport uint32_t, uint64_t, int64_t, uint8_t, uint16_t
+from libc.stdint cimport uint32_t, uint64_t, int64_t, uint8_t, uint16_t, int8_t
 from libc.stdio cimport FILE
 
 cdef extern from "../../third_party/msgpuck/msgpuck.h":
@@ -43,6 +43,7 @@ cdef extern from "../../third_party/msgpuck/msgpuck.h":
     cdef char *mp_encode_int(char *data, int64_t num)
     cdef int64_t mp_decode_int(const char **data)
 
+    cdef uint32_t mp_sizeof_float(float num)
     cdef char *mp_encode_float(char *data, float num)
     cdef float mp_decode_float(const char **data)
 
@@ -64,6 +65,13 @@ cdef extern from "../../third_party/msgpuck/msgpuck.h":
     cdef char *mp_encode_bin(char *data, const char *str, uint32_t len)
     cdef const char *mp_decode_bin(const char **data, uint32_t *len)
 
+    cdef uint32_t mp_sizeof_extl(uint32_t len)
+    cdef uint32_t mp_sizeof_ext(uint32_t len)
+    cdef char *mp_encode_extl(char *data, int8_t type, uint32_t len)
+    cdef char *mp_encode_ext(char *data, int8_t type, char *str, uint32_t len)
+    cdef uint32_t mp_decode_extl(const char **data, int8_t *type)
+    cdef const char *mp_decode_ext(const char **data, int8_t *type, uint32_t *len)
+
     cdef uint32_t mp_decode_strbinl(const char **data)
     cdef const char *mp_decode_strbin(const char **data, uint32_t *len)
 
diff --git a/asynctnt/iproto/const.pxi b/asynctnt/iproto/const.pxi
index 9d3adf9..5be9b6d 100644
--- a/asynctnt/iproto/const.pxi
+++ b/asynctnt/iproto/const.pxi
@@ -4,7 +4,7 @@ DEF _BUFFER_MAX_GROW = 65536
 
 DEF _DEALLOCATE_RATIO = 4
 
-DEF TNT_FIELDS_FREELIST_SIZE = 128
+DEF METADATA_FREELIST_SIZE = 128
 DEF REQUEST_FREELIST = 256
 
 # Header length description:
@@ -15,8 +15,9 @@ DEF REQUEST_FREELIST = 256
 # mp_sizeof_uint(TP_SYNC) +
 # sync len +
 # mp_sizeof_uint(TP_SCHEMA_ID) +
+# mp_sizeof_uint(STREAM_ID) +
 # schema_id len
-DEF HEADER_CONST_LEN = 5 + 1 + 1 + 1 + 1 + 5 + 1 + 9
+DEF HEADER_CONST_LEN = 5 + 1 + 1 + 1 + 1 + 5 + 1 + 9 + 9
 
 DEF IPROTO_GREETING_SIZE = 128
 DEF TARANTOOL_VERSION_LENGTH = 64
@@ -26,3 +27,5 @@ DEF SCRAMBLE_SIZE = 20
 
 DEF SPACE_VSPACE = 281
 DEF SPACE_VINDEX = 289
+
+DEF DATETIME_TAIL_SZ = 4 + 2 + 2
diff --git a/asynctnt/iproto/coreproto.pyx b/asynctnt/iproto/coreproto.pyx
index 5841a75..e501b4a 100644
--- a/asynctnt/iproto/coreproto.pyx
+++ b/asynctnt/iproto/coreproto.pyx
@@ -37,7 +37,7 @@ cdef class CoreProtocol:
         initial_read_buffer_size = initial_read_buffer_size or 0x20000
         self.transport = None
 
-        self.rbuf = ReadBuffer.new(encoding, initial_read_buffer_size)
+        self.rbuf = ReadBuffer.create(encoding, initial_read_buffer_size)
         self.state = PROTOCOL_IDLE
         self.con_state = CONNECTION_BAD
 
@@ -181,7 +181,6 @@ cdef class CoreProtocol:
 
     def connection_lost(self, exc):
         self.con_state = CONNECTION_BAD
-        # self.schema = None
         self.version = None
         self.salt = None
         self.rbuf = None
diff --git a/asynctnt/iproto/db.pxd b/asynctnt/iproto/db.pxd
index d92e137..eea8c64 100644
--- a/asynctnt/iproto/db.pxd
+++ b/asynctnt/iproto/db.pxd
@@ -1,51 +1,92 @@
+cimport cython
 from libc.stdint cimport uint64_t, uint32_t
 
-
+@cython.final
 cdef class Db:
     cdef:
+        uint64_t _stream_id
         BaseProtocol _protocol
         bytes _encoding
 
     @staticmethod
-    cdef inline Db new(BaseProtocol protocol)
+    cdef inline Db create(BaseProtocol protocol, uint64_t stream_id)
 
     cdef inline uint64_t next_sync(self)
 
-    cdef object _ping(self, float timeout, bint push_subscribe, bint check_schema_change)
-
-    cdef object _call16(self, str func_name, args,
-                        float timeout, bint push_subscribe, bint check_schema_change)
-
-    cdef object _call(self, str func_name, args,
-                      float timeout, bint push_subscribe, bint check_schema_change)
-
-    cdef object _eval(self, str expression, args,
-                      float timeout, bint push_subscribe, bint check_schema_change)
-
-    cdef object _select(self, SchemaSpace space, SchemaIndex index, key,
-                        uint64_t offset, uint64_t limit, uint32_t iterator,
-                        float timeout, bint push_subscribe, bint check_schema_change)
-
-    cdef object _insert(self, SchemaSpace space, t, bint replace,
-                        float timeout, bint push_subscribe, bint check_schema_change)
-
-    cdef object _delete(self, SchemaSpace space, SchemaIndex index, key,
-                        float timeout, bint push_subscribe, bint check_schema_change)
-
-    cdef object _update(self, SchemaSpace space, SchemaIndex index, key,
-                        list operations, float timeout, bint push_subscribe, bint check_schema_change)
-
-    cdef object _upsert(self, SchemaSpace space, t, list operations,
-                        float timeout, bint push_subscribe, bint check_schema_change)
-
-    cdef object _sql(self, str query, args, bint parse_metadata,
-                     float timeout, bint push_subscribe, bint check_schema_change)
-
-    cdef object _auth(self, bytes salt, str username, str password,
-                      float timeout, bint push_subscribe, bint check_schema_change)
-
-    @staticmethod
-    cdef bytes _sha1(tuple values)
-
-    @staticmethod
-    cdef bytes _strxor(bytes hash1, bytes scramble)
+    cdef object _ping(self, float timeout)
+
+    cdef object _id(self, float timeout)
+
+    cdef object _auth(self,
+                      bytes salt,
+                      str username,
+                      str password,
+                      float timeout)
+
+    cdef object _call(self,
+                      tarantool.iproto_type op,
+                      str func_name,
+                      object args,
+                      float timeout,
+                      bint push_subscribe)
+
+    cdef object _eval(self,
+                      str expression,
+                      object args,
+                      float timeout,
+                      bint push_subscribe)
+
+    cdef object _select(self,
+                        object space,
+                        object index,
+                        object key,
+                        uint64_t offset,
+                        uint64_t limit,
+                        object iterator,
+                        float timeout,
+                        bint check_schema_change)
+
+    cdef object _insert(self,
+                        object space,
+                        object t,
+                        bint replace,
+                        float timeout)
+
+    cdef object _delete(self,
+                        object space,
+                        object index,
+                        object key,
+                        float timeout)
+
+    cdef object _update(self,
+                        object space,
+                        object index,
+                        object key,
+                        list operations,
+                        float timeout)
+
+    cdef object _upsert(self,
+                        object space,
+                        object t,
+                        list operations,
+                        float timeout)
+
+    cdef object _execute(self,
+                         query,
+                         object args,
+                         bint parse_metadata,
+                         float timeout)
+
+    cdef object _prepare(self,
+                         query,
+                         bint parse_metadata,
+                         float timeout)
+
+    cdef object _begin(self,
+                       uint32_t isolation,
+                       double tx_timeout,
+                       float timeout)
+
+    cdef object _commit(self, float timeout)
+
+    cdef object _rollback(self, float timeout)
diff --git a/asynctnt/iproto/db.pyx b/asynctnt/iproto/db.pyx
index 93cc895..73f9fec 100644
--- a/asynctnt/iproto/db.pyx
+++ b/asynctnt/iproto/db.pyx
@@ -1,15 +1,16 @@
-cimport cpython
-
-import hashlib
+cimport cython
 
+@cython.final
 cdef class Db:
     def __cinit__(self):
+        self._stream_id = 0
         self._protocol = None
         self._encoding = None
 
     @staticmethod
-    cdef inline Db new(BaseProtocol protocol):
+    cdef inline Db create(BaseProtocol protocol, uint64_t stream_id):
         cdef Db db = Db.__new__(Db)
+        db._stream_id = stream_id
         db._protocol = protocol
         db._encoding = protocol.encoding
         return db
@@ -17,348 +18,417 @@ cdef class Db:
     cdef inline uint64_t next_sync(self):
         return self._protocol.next_sync()
 
-    cdef object _ping(self,
-                      float timeout,
-                      bint push_subscribe,
-                      bint check_schema_change):
-        cdef:
-            tarantool.iproto_type op
-            uint64_t sync
-            int64_t schema_id
-            WriteBuffer buf
-
-        op = tarantool.IPROTO_PING
-        sync = self.next_sync()
-        schema_id = -1  # not sending schema_id with the request
-        buf = WriteBuffer.new(self._encoding)
-        buf.write_header(sync, op, schema_id)
-        buf.write_length()
-        return self._protocol.execute(
-            Request.new(op, sync, schema_id, None, push_subscribe, check_schema_change),
-            buf, timeout)
-
-    cdef object _call16(self,
-                        str func_name,
-                        args,
-                        float timeout,
-                        bint push_subscribe,
-                        bint check_schema_change):
+    cdef object _ping(self, float timeout):
+        cdef PingRequest req = PingRequest.__new__(PingRequest)
+        req.op = tarantool.IPROTO_PING
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.check_schema_change = True
+        return self._protocol.execute(self._protocol, req, timeout)
+
+    cdef object _id(self, float timeout):
+        cdef IDRequest req = IDRequest.__new__(IDRequest)
+        req.op = tarantool.IPROTO_ID
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.check_schema_change = False
+        return self._protocol.execute(self._protocol, req, timeout)
+
+    cdef object _auth(self,
+                      bytes salt,
+                      str username,
+                      str password,
+                      float timeout):
         cdef:
-            tarantool.iproto_type op
-            uint64_t sync
-            int64_t schema_id
-            WriteBuffer buf
-
-        op = tarantool.IPROTO_CALL_16
-        sync = self.next_sync()
-        schema_id = -1  # not sending schema_id with the request
-        buf = WriteBuffer.new(self._encoding)
-        buf.write_header(sync, op, schema_id)
-        buf.encode_request_call(func_name, args)
-        buf.write_length()
-        return self._protocol.execute(
-            Request.new(op, sync, schema_id, None, push_subscribe, check_schema_change),
-            buf, timeout)
+            AuthRequest req
+
+        req = AuthRequest.__new__(AuthRequest)
+        req.op = tarantool.IPROTO_AUTH
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.salt = salt
+        req.username = username
+        req.password = password
+        req.push_subscribe = False
+        req.parse_as_tuples = False
+        req.parse_metadata = False
+        req.check_schema_change = False
+
+        return self._protocol.execute(self._protocol, req, timeout)
 
     cdef object _call(self,
+                      tarantool.iproto_type op,
                       str func_name,
-                      args,
+                      object args,
                       float timeout,
-                      bint push_subscribe,
-                      bint check_schema_change):
-        cdef:
-            tarantool.iproto_type op
-            uint64_t sync
-            int64_t schema_id
-            WriteBuffer buf
-
-        op = tarantool.IPROTO_CALL
-        sync = self.next_sync()
-        schema_id = -1  # not sending schema_id with the request
-        buf = WriteBuffer.new(self._encoding)
-        buf.write_header(sync, op, schema_id)
-        buf.encode_request_call(func_name, args)
-        buf.write_length()
-        return self._protocol.execute(
-            Request.new(op, sync, schema_id, None, push_subscribe, check_schema_change),
-            buf, timeout)
+                      bint push_subscribe):
+        cdef CallRequest req = CallRequest.__new__(CallRequest)
+        req.op = op
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.func_name = func_name
+        req.args = args
+        req.push_subscribe = push_subscribe
+        req.check_schema_change = True
+        return self._protocol.execute(self._protocol, req, timeout)
 
     cdef object _eval(self,
                       str expression,
-                      args,
+                      object args,
                       float timeout,
-                      bint push_subscribe,
-                      bint check_schema_change):
-        cdef:
-            tarantool.iproto_type op
-            uint64_t sync
-            int64_t schema_id
-            WriteBuffer buf
-
-        op = tarantool.IPROTO_EVAL
-        sync = self.next_sync()
-        schema_id = -1  # not sending schema_id with the request
-        buf = WriteBuffer.new(self._encoding)
-        buf.write_header(sync, op, schema_id)
-        buf.encode_request_eval(expression, args)
-        buf.write_length()
-        return self._protocol.execute(
-            Request.new(op, sync, schema_id, None, push_subscribe, check_schema_change),
-            buf, timeout)
-
-    cdef object _select(self, SchemaSpace space, SchemaIndex index, key,
-                         uint64_t offset, uint64_t limit, uint32_t iterator,
-                         float timeout, bint push_subscribe, bint check_schema_change):
-        cdef:
-            tarantool.iproto_type op
-            uint64_t sync
-            int64_t schema_id
-            WriteBuffer buf
-
-        op = tarantool.IPROTO_SELECT
-        sync = self.next_sync()
-        schema_id = -1  # not sending schema_id with the request
-        buf = WriteBuffer.new(self._encoding)
-        buf.write_header(sync, op, schema_id)
-        buf.encode_request_select(space, index, key,
-                                  offset, limit, iterator)
-        buf.write_length()
-        return self._protocol.execute(
-            Request.new(op, sync, schema_id, space, push_subscribe, check_schema_change),
-            buf, timeout)
-
-    cdef object _insert(self, SchemaSpace space, t, bint replace,
-                         float timeout, bint push_subscribe, bint check_schema_change):
-        cdef:
-            tarantool.iproto_type op
-            uint64_t sync
-            int64_t schema_id
-            WriteBuffer buf
-
-        op = tarantool.IPROTO_REPLACE if replace else tarantool.IPROTO_INSERT
-        sync = self.next_sync()
-        schema_id = -1  # not sending schema_id with the request
-        buf = WriteBuffer.new(self._encoding)
-        buf.write_header(sync, op, schema_id)
-        buf.encode_request_insert(space, t)
-        buf.write_length()
-        return self._protocol.execute(
-            Request.new(op, sync, schema_id, space, push_subscribe, check_schema_change),
-            buf, timeout)
-
-    cdef object _delete(self, SchemaSpace space, SchemaIndex index, key,
-                         float timeout, bint push_subscribe, bint check_schema_change):
-        cdef:
-            tarantool.iproto_type op
-            uint64_t sync
-            int64_t schema_id
-            WriteBuffer buf
-
-        op = tarantool.IPROTO_DELETE
-        sync = self.next_sync()
-        schema_id = -1  # not sending schema_id with the request
-        buf = WriteBuffer.new(self._encoding)
-        buf.write_header(sync, op, schema_id)
-        buf.encode_request_delete(space, index, key)
-        buf.write_length()
-        return self._protocol.execute(
-            Request.new(op, sync, schema_id, space, push_subscribe, check_schema_change),
-            buf, timeout)
-
-    cdef object _update(self, SchemaSpace space, SchemaIndex index,
-                         key, list operations,
-                         float timeout, bint push_subscribe, bint check_schema_change):
-        cdef:
-            tarantool.iproto_type op
-            uint64_t sync
-            int64_t schema_id
-            WriteBuffer buf
-
-        op = tarantool.IPROTO_UPDATE
-        sync = self.next_sync()
-        schema_id = -1  # not sending schema_id with the request
-        buf = WriteBuffer.new(self._encoding)
-        buf.write_header(sync, op, schema_id)
-        buf.encode_request_update(space, index, key, operations)
-        buf.write_length()
-        return self._protocol.execute(
-            Request.new(op, sync, schema_id, space, push_subscribe, check_schema_change),
-            buf, timeout)
-
-    cdef object _upsert(self, SchemaSpace space, t, list operations,
-                         float timeout, bint push_subscribe, bint check_schema_change):
-        cdef:
-            tarantool.iproto_type op
-            uint64_t sync
-            int64_t schema_id
-            WriteBuffer buf
-
-        op = tarantool.IPROTO_UPSERT
-        sync = self.next_sync()
-        schema_id = -1  # not sending schema_id with the request
-        buf = WriteBuffer.new(self._encoding)
-        buf.write_header(sync, op, schema_id)
-        buf.encode_request_upsert(space, t, operations)
-        buf.write_length()
-        return self._protocol.execute(
-            Request.new(op, sync, schema_id, space, push_subscribe, check_schema_change),
-            buf, timeout)
-
-    cdef object _sql(self, str query, args, bint parse_metadata,
-                      float timeout, bint push_subscribe, bint check_schema_change):
-        cdef:
-            tarantool.iproto_type op
-            uint64_t sync
-            int64_t schema_id
-            WriteBuffer buf
-            Request req
-
-        op = tarantool.IPROTO_EXECUTE
-        sync = self.next_sync()
-        schema_id = -1  # not sending schema_id with the request
-        buf = WriteBuffer.new(self._encoding)
-        buf.write_header(sync, op, schema_id)
-        buf.encode_request_sql(query, args)
-        buf.write_length()
-        req = Request.new(op, sync, schema_id, None, push_subscribe, check_schema_change)
-        req.parse_metadata = parse_metadata
-        req.parse_as_tuples = True
-        return self._protocol.execute(req, buf, timeout)
-
-    cdef object _auth(self, bytes salt, str username, str password,
-                       float timeout, bint push_subscribe, bint check_schema_change):
+                      bint push_subscribe):
+        cdef EvalRequest req = EvalRequest.__new__(EvalRequest)
+        req.op = tarantool.IPROTO_EVAL
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.expression = expression
+        req.args = args
+        req.push_subscribe = push_subscribe
+        req.check_schema_change = True
+        return self._protocol.execute(self._protocol, req, timeout)
+
+    cdef object _select(self,
+                        object space,
+                        object index,
+                        object key,
+                        uint64_t offset,
+                        uint64_t limit,
+                        object iterator,
+                        float timeout,
+                        bint check_schema_change):
         cdef:
-            tarantool.iproto_type op
-            uint64_t sync
-            int64_t schema_id
-            WriteBuffer buf
-
-            bytes username_bytes, password_bytes
-            bytes hash1, hash2, scramble
-
-        op = tarantool.IPROTO_AUTH
-        sync = self.next_sync()
-        schema_id = -1  # not sending schema_id with the request
-        buf = WriteBuffer.new(self._encoding)
-        buf.write_header(sync, op, schema_id)
-
-        username_bytes = encode_unicode_string(username, self._encoding)
-        password_bytes = encode_unicode_string(password, self._encoding)
-
-        hash1 = Db._sha1((password_bytes,))
-        hash2 = Db._sha1((hash1,))
-        scramble = Db._sha1((salt, hash2))
-        scramble = Db._strxor(hash1, scramble)
+            SchemaSpace sp
+            SchemaIndex idx
+            SelectRequest req
+            uint32_t iterator_value
 
-        buf.encode_request_auth(username_bytes, scramble)
+        sp = self._protocol._schema.get_or_create_space(space)
+        idx = sp.get_index(index)
 
-        buf.write_length()
-        return self._protocol.execute(
-            Request.new(op, sync, schema_id, None, push_subscribe, check_schema_change),
-            buf, timeout)
+        iterator_value = self._protocol.transform_iterator(iterator)
+        if key is None and isinstance(iterator, int):
+            iterator_value = <uint32_t> iterator
+            if iterator_value == 0:
+                iterator_value = 2  # ALL
+
+        req = SelectRequest.__new__(SelectRequest)
+        req.op = tarantool.IPROTO_SELECT
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.space = sp
+        req.index = idx
+        req.key = key
+        req.offset = offset
+        req.limit = limit
+        req.iterator = iterator_value
+        req.push_subscribe = False
+        req.check_schema_change = check_schema_change
+        req.parse_as_tuples = True
 
-    @staticmethod
-    cdef bytes _sha1(tuple values):
-        cdef object sha = hashlib.sha1()
-        for i in values:
-            if i is not None:
-                sha.update(i)
-        return sha.digest()
+        return self._protocol.execute(self._protocol, req, timeout)
 
-    @staticmethod
-    cdef bytes _strxor(bytes hash1, bytes scramble):
+    cdef object _insert(self,
+                        object space,
+                        object t,
+                        bint replace,
+                        float timeout):
         cdef:
-            char *hash1_str
-            ssize_t hash1_len
-
-            char *scramble_str
-            ssize_t scramble_len
-        cpython.bytes.PyBytes_AsStringAndSize(hash1,
-                                              &hash1_str, &hash1_len)
-        cpython.bytes.PyBytes_AsStringAndSize(scramble,
-                                              &scramble_str, &scramble_len)
-        for i in range(scramble_len):
-            scramble_str[i] = hash1_str[i] ^ scramble_str[i]
-        return scramble
-
-    # public methods
-
-    def ping(self, timeout=-1):
-        return self._ping(timeout, <bint> False, <bint> True)
+            SchemaSpace sp
+            InsertRequest req
 
-    def call16(self, func_name, args=None, timeout=-1, push_subscribe=False):
-        return self._call16(func_name, args, timeout,
-                            <bint> push_subscribe, <bint> True)
+        sp = self._protocol._schema.get_or_create_space(space)
 
-    def call(self, func_name, args=None, timeout=-1, push_subscribe=False):
-        return self._call(func_name, args, timeout,
-                          <bint> push_subscribe, <bint> True)
+        req = InsertRequest.__new__(InsertRequest)
+        req.op = tarantool.IPROTO_REPLACE if replace else tarantool.IPROTO_INSERT
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.space = sp
+        req.t = t
+        req.push_subscribe = False
+        req.check_schema_change = True
+        req.parse_as_tuples = True
 
-    def eval(self, expression, args=None, timeout=-1, push_subscribe=False):
-        return self._eval(expression, args, timeout,
-                          <bint> push_subscribe, <bint> True)
+        return self._protocol.execute(self._protocol, req, timeout)
 
-    def select(self, space, key=None,
-               offset=0, limit=0xffffffff, index=0, iterator=0,
-               timeout=-1, check_schema_change=True):
+    cdef object _delete(self,
+                        object space,
+                        object index,
+                        object key,
+                        float timeout):
         cdef:
             SchemaSpace sp
             SchemaIndex idx
+            DeleteRequest req
+
         sp = self._protocol._schema.get_or_create_space(space)
         idx = sp.get_index(index)
 
-        iterator = self._protocol.transform_iterator(iterator)
-        if key is None and iterator == 0:
-            iterator = 2  # ALL
+        req = DeleteRequest.__new__(DeleteRequest)
+        req.op = tarantool.IPROTO_DELETE
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.space = sp
+        req.index = idx
+        req.key = key
+        req.push_subscribe = False
+        req.check_schema_change = True
+        req.parse_as_tuples = True
 
-        return self._select(sp, idx, key, offset, limit, iterator,
-                            timeout, <bint> False, <bint> check_schema_change)
+        return self._protocol.execute(self._protocol, req, timeout)
 
-    def insert(self, space, t, replace=False,
-               timeout=-1):
+    cdef object _update(self,
+                        object space,
+                        object index,
+                        object key,
+                        list operations,
+                        float timeout):
         cdef:
             SchemaSpace sp
-        sp = self._protocol._schema.get_or_create_space(space)
-
-        return self._insert(sp, t, <bint> replace, timeout,
-                            <bint> False, <bint> True)
+            SchemaIndex idx
+            UpdateRequest req
 
-    def replace(self, space, t, timeout=-1):
-        cdef:
-            SchemaSpace sp
         sp = self._protocol._schema.get_or_create_space(space)
+        idx = sp.get_index(index)
 
-        return self._insert(sp, t, <bint> True, timeout,
-                            <bint> False, <bint> True)
+        req = UpdateRequest.__new__(UpdateRequest)
+        req.op = tarantool.IPROTO_UPDATE
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.space = sp
+        req.index = idx
+        req.key = key
+        req.operations = operations
+        req.push_subscribe = False
+        req.check_schema_change = True
+        req.parse_as_tuples = True
+
+        return self._protocol.execute(self._protocol, req, timeout)
 
-    def delete(self, space, key, index=0, timeout=-1):
+    cdef object _upsert(self,
+                        object space,
+                        object t,
+                        list operations,
+                        float timeout):
         cdef:
             SchemaSpace sp
-            SchemaIndex idx
+            UpsertRequest req
+
         sp = self._protocol._schema.get_or_create_space(space)
-        idx = sp.get_index(index)
 
-        return self._delete(sp, idx, key, timeout,
-                            <bint> False, <bint> True)
+        req = UpsertRequest.__new__(UpsertRequest)
+        req.op = tarantool.IPROTO_UPSERT
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.space = sp
+        req.t = t
+        req.operations = operations
+        req.push_subscribe = False
+        req.check_schema_change = True
+        req.parse_as_tuples = True
+
+        return self._protocol.execute(self._protocol, req, timeout)
 
-    def update(self, space, key, operations, index=0, timeout=-1):
+    cdef object _execute(self,
+                         object query,
+                         object args,
+                         bint parse_metadata,
+                         float timeout):
         cdef:
-            SchemaSpace sp
-            SchemaIndex idx
-        sp = self._protocol._schema.get_or_create_space(space)
-        idx = sp.get_index(index)
+            ExecuteRequest req
+
+        req = ExecuteRequest.__new__(ExecuteRequest)
+        req.op = tarantool.IPROTO_EXECUTE
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+
+        if isinstance(query, str):
+            req.query = query
+            req.statement_id = 0
+        elif isinstance(query, int):
+            req.query = None
+            req.statement_id = <uint64_t> query
+        else:
+            raise TypeError('query must be either str or int')
+
+        req.args = args
+        req.parse_metadata = parse_metadata
+        req.push_subscribe = False
+        req.check_schema_change = True
+        req.parse_as_tuples = True
 
-        return self._update(sp, idx, key, operations, timeout,
-                            <bint> False, <bint> True)
+        return self._protocol.execute(self._protocol, req, timeout)
 
-    def upsert(self, space, t, operations, timeout=-1):
+    cdef object _prepare(self,
+                         object query,
+                         bint parse_metadata,
+                         float timeout):
         cdef:
-            SchemaSpace sp
-        sp = self._protocol._schema.get_or_create_space(space)
+            PrepareRequest req
+
+        req = PrepareRequest.__new__(PrepareRequest)
+        req.op = tarantool.IPROTO_PREPARE
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+
+        if isinstance(query, str):
+            req.query = query
+            req.statement_id = 0
+        elif isinstance(query, int):
+            req.query = None
+            req.statement_id = <uint64_t> query
+        else:
+            raise TypeError('query must be either str or int')
+
+        req.push_subscribe = False
+        req.check_schema_change = True
+        req.parse_as_tuples = True
+        req.parse_metadata = parse_metadata
+
+        return self._protocol.execute(self._protocol, req, timeout)
+
+    cdef object _begin(self,
+                       uint32_t isolation,
+                       double tx_timeout,
+                       float timeout):
+        cdef BeginRequest req = BeginRequest.__new__(BeginRequest)
+        req.op = tarantool.IPROTO_BEGIN
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.check_schema_change = True
+        req.push_subscribe = False
+        req.isolation = isolation
+        req.tx_timeout = tx_timeout
+        return self._protocol.execute(self._protocol, req, timeout)
+
+    cdef object _commit(self, float timeout):
+        cdef CommitRequest req = CommitRequest.__new__(CommitRequest)
+        req.op = tarantool.IPROTO_COMMIT
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.check_schema_change = True
+        req.push_subscribe = False
+        return self._protocol.execute(self._protocol, req, timeout)
+
+    cdef object _rollback(self, float timeout):
+        cdef RollbackRequest req = RollbackRequest.__new__(RollbackRequest)
+        req.op = tarantool.IPROTO_ROLLBACK
+        req.sync = self.next_sync()
+        req.stream_id = self._stream_id
+        req.check_schema_change = True
+        return self._protocol.execute(self._protocol, req, timeout)
 
-        return self._upsert(sp, t, operations, timeout,
-                            <bint> False, <bint> True)
+    # public methods
 
-    def sql(self, query, args, parse_metadata=True, timeout=-1):
-        return self._sql(query, args, <bint> parse_metadata, timeout,
-                         <bint> False, <bint> True)
+    @property
+    def stream_id(self):
+        return <int> self._stream_id
+
+    def set_stream_id(self, int stream_id):
+        self._stream_id = <uint64_t> stream_id
+
+    def ping(self, float timeout=-1):
+        return self._ping(timeout)
+
+    def call16(self,
+               str func_name,
+               object args=None,
+               float timeout=-1,
+               bint push_subscribe=False):
+        return self._call(tarantool.IPROTO_CALL_16,
+                          func_name,
+                          args,
+                          timeout,
+                          <bint> push_subscribe)
+
+    def call(self,
+             str func_name,
+             object args=None,
+             float timeout=-1,
+             bint push_subscribe=False):
+        return self._call(tarantool.IPROTO_CALL,
+                          func_name,
+                          args,
+                          timeout,
+                          <bint> push_subscribe)
+
+    def eval(self,
+             str expression,
+             object args=None,
+             float timeout=-1,
+             bint push_subscribe=False):
+        return self._eval(expression,
+                          args,
+                          timeout,
+                          <bint> push_subscribe)
+
+    def select(self,
+               object space,
+               object key=None,
+               int offset=0,
+               int limit=0xffffffff,
+               object index=0,
+               object iterator=0,
+               float timeout=-1,
+               bint check_schema_change=True):
+        return self._select(space, index, key, offset, limit, iterator,
+                            timeout, check_schema_change)
+
+    def insert(self,
+               object space,
+               object t,
+               bint replace=False,
+               float timeout=-1):
+        return self._insert(space, t, <bint> replace, timeout)
+
+    def replace(self,
+                object space,
+                object t,
+                float timeout=-1):
+        return self._insert(space, t, <bint> True, timeout)
+
+    def delete(self,
+               object space,
+               object key,
+               object index=0,
+               float timeout=-1):
+        return self._delete(space, index, key, timeout)
+
+    def update(self,
+               object space,
+               object key,
+               list operations,
+               object index=0,
+               float timeout=-1):
+        return self._update(space, index, key, operations, timeout)
+
+    def upsert(self,
+               object space,
+               object t,
+               list operations,
+               float timeout=-1):
+        return self._upsert(space, t, operations, timeout)
+
+    def execute(self,
+                object query,
+                object args,
+                bint parse_metadata=True,
+                float timeout=-1):
+        return self._execute(query, args, <bint> parse_metadata, timeout)
+
+    def prepare(self,
+                object query,
+                bint parse_metadata=True,
+                float timeout=-1):
+        return self._prepare(query, <bint> parse_metadata, timeout)
+
+    def begin(self,
+              uint32_t isolation,
+              float tx_timeout=0,
+              float timeout=-1):
+        return self._begin(isolation, <double> tx_timeout, timeout)
+
+    def commit(self, float timeout=-1):
+        return self._commit(timeout)
+
+    def rollback(self, float timeout=-1):
+        return self._rollback(timeout)
diff --git a/asynctnt/iproto/ext.pxd b/asynctnt/iproto/ext.pxd
new file mode 100644
index 0000000..e173ce8
--- /dev/null
+++ b/asynctnt/iproto/ext.pxd
@@ -0,0 +1,31 @@
+from libc.stdint cimport uint32_t, uint8_t, int64_t, int32_t, int16_t
+from libc cimport math
+from cpython.datetime cimport datetime
+
+cdef inline uint32_t bcd_len(uint32_t digits_len):
+    return <uint32_t> math.floor(digits_len / 2) + 1
+
+cdef uint32_t decimal_len(int exponent, uint32_t digits_count)
+cdef char *decimal_encode(char *p,
+                          uint32_t digits_count,
+                          uint8_t sign,
+                          tuple digits,
+                          int exponent) except NULL
+cdef object decimal_decode(const char ** p, uint32_t length)
+
+cdef object uuid_decode(const char ** p, uint32_t length)
+
+cdef struct IProtoDateTime:
+    int64_t seconds
+    int32_t nsec
+    int16_t tzoffset
+    int16_t tzindex
+
+cdef void datetime_zero(IProtoDateTime *dt)
+cdef uint32_t datetime_len(IProtoDateTime *dt)
+cdef char *datetime_encode(char *p, IProtoDateTime *dt) except NULL
+cdef int datetime_decode(const char ** p,
+                         uint32_t length,
+                         IProtoDateTime *dt) except -1
+cdef void datetime_from_py(datetime ob, IProtoDateTime *dt)
+cdef object datetime_to_py(IProtoDateTime *dt)
diff --git a/asynctnt/iproto/ext.pyx b/asynctnt/iproto/ext.pyx
new file mode 100644
index 0000000..a9123a1
--- /dev/null
+++ b/asynctnt/iproto/ext.pyx
@@ -0,0 +1,211 @@
+from libc.stdint cimport uint32_t
+from cpython.datetime cimport PyDateTimeAPI, timedelta_new, datetime, datetime_tzinfo
+cimport cpython.datetime
+from libc.string cimport memcpy
+
+from uuid import UUID
+from decimal import Decimal
+
+cdef uint32_t decimal_len(int exponent, uint32_t digits_count):
+    cdef:
+        uint32_t length
+
+    length = bcd_len(digits_count)
+    if exponent > 0:
+        length += mp_sizeof_int(-exponent)
+    else:
+        length += mp_sizeof_uint(-exponent)
+
+    return length
+
+cdef char *decimal_encode(char *p,
+                          uint32_t digits_count,
+                          uint8_t sign,
+                          tuple digits,
+                          int exponent) except NULL:
+    cdef:
+        int i
+        uint8_t byte
+        char *out
+        uint32_t length
+
+    # encode exponent
+    if exponent > 0:
+        p = mp_encode_int(p, -exponent)
+    else:
+        p = mp_encode_uint(p, -exponent)
+
+    length = bcd_len(digits_count)
+
+    out = &p[length - 1]
+    if sign == 1:
+        byte = 0x0d
+    else:
+        byte = 0x0c
+
+    i = digits_count - 1
+    while out >= p:
+        if i >= 0:
+            byte |= (<uint8_t> <object> cpython.tuple.PyTuple_GET_ITEM(digits, i)) << 4
+
+        out[0] = byte
+        byte = 0
+
+        if i > 0:
+            byte = <uint8_t> <object> cpython.tuple.PyTuple_GET_ITEM(digits, i - 1) & 0xf
+
+        out -= 1
+        i -= 2
+
+    p = &p[length]
+    return p
+
+cdef object decimal_decode(const char ** p, uint32_t length):
+    cdef:
+        int exponent
+        uint8_t sign
+        mp_type obj_type
+        const char *first
+        const char *last
+        uint32_t digits_count
+        uint8_t nibble
+
+    sign = 0
+    first = &p[0][0]
+    last = first + length - 1
+
+    # decode exponent
+    obj_type = mp_typeof(p[0][0])
+    if obj_type == MP_UINT:
+        exponent = -<int> mp_decode_uint(p)
+    elif obj_type == MP_INT:
+        exponent = -<int> mp_decode_int(p)
+    else:
+        raise TypeError('unexpected exponent type: {}'.format(obj_type))
+
+    length -= (&p[0][0] - first)
+    first = &p[0][0]
+
+    while first[0] == 0:
+        first += 1  # skipping leading zeros
+
+    sign = last[0] & 0xf  # extract sign
+    if sign == 0x0a or sign == 0x0c or sign == 0x0e or sign == 0x0f:
+        sign = 0
+    else:
+        sign = 1
+
+    # decode digits
+    digits_count = (last - first) * 2 + 1
+    if first[0] & 0xf0 == 0:
+        digits_count -= 1  # adjust for leading zero nibble
+
+    digits = cpython.tuple.PyTuple_New(digits_count)
+
+    if digits_count > 0:
+        while True:
+            nibble = (last[0] & 0xf0) >> 4  # left nibble first
+            item = <object> <int> nibble
+            cpython.Py_INCREF(item)
+            cpython.tuple.PyTuple_SET_ITEM(digits, digits_count - 1, item)
+
+            digits_count -= 1
+            if digits_count == 0:
+                break
+            last -= 1
+
+            nibble = last[0] & 0x0f  # right nibble
+            item = <object> <int> nibble
+            cpython.Py_INCREF(item)
+            cpython.tuple.PyTuple_SET_ITEM(digits, digits_count - 1, item)
+
+            digits_count -= 1
+            if digits_count == 0:
+                break
+
+    p[0] += length
+
+    return Decimal((<object> <int> sign, digits, <object> exponent))
+
+cdef object uuid_decode(const char ** p, uint32_t length):
+    data = cpython.bytes.PyBytes_FromStringAndSize(p[0], length)
+    p[0] += length
+    return UUID(bytes=data)
+
+cdef inline void datetime_zero(IProtoDateTime *dt):
+    dt.seconds = 0
+    dt.nsec = 0
+    dt.tzoffset = 0
+    dt.tzindex = 0
+
+cdef inline uint32_t datetime_len(IProtoDateTime *dt):
+    cdef uint32_t sz
+    sz = sizeof(int64_t)
+    if dt.nsec != 0 or dt.tzoffset != 0 or dt.tzindex != 0:
+        return sz + DATETIME_TAIL_SZ
+    return sz
+
+cdef char *datetime_encode(char *p, IProtoDateTime *dt) except NULL:
+    store_u64(p, dt.seconds)
+    p += sizeof(dt.seconds)
+    if dt.nsec != 0 or dt.tzoffset != 0 or dt.tzindex != 0:
+        memcpy(p, &dt.nsec, DATETIME_TAIL_SZ)
+        p += DATETIME_TAIL_SZ
+    return p
+
+cdef int datetime_decode(
+        const char ** p,
+        uint32_t length,
+        IProtoDateTime *dt
+) except -1:
+    delta = None
+    tz = None
+
+    dt.seconds = load_u64(p[0])
+    p[0] += sizeof(dt.seconds)
+    length -= sizeof(dt.seconds)
+
+    if length == 0:
+        return 0
+
+    if length != DATETIME_TAIL_SZ:
+        raise ValueError("invalid datetime size. got {} extra bytes".format(
+            length
+        ))
+
+    dt.nsec = load_u32(p[0])
+    p[0] += 4
+    dt.tzoffset = load_u16(p[0])
+    p[0] += 2
+    dt.tzindex = load_u16(p[0])
+    p[0] += 2
+
+cdef void datetime_from_py(datetime ob, IProtoDateTime *dt):
+    cdef:
+        double ts
+        int offset
+    ts = <double> ob.timestamp()
+    dt.seconds = <int64_t> ts
+    dt.nsec = <int32_t> ((ts - <double> dt.seconds) * 1000000) * 1000
+
+    if datetime_tzinfo(ob) is not None:
+        offset = ob.utcoffset().total_seconds()
+        dt.tzoffset = <int16_t> (offset / 60)
+
+cdef object datetime_to_py(IProtoDateTime *dt):
+    cdef:
+        double timestamp
+        object tz
+
+    tz = None
+
+    if dt.tzoffset != 0:
+        delta = timedelta_new(0, <int> dt.tzoffset * 60, 0)
+        tz = timezone_new(delta)
+
+    timestamp = dt.seconds + (<double> dt.nsec) / 1e9
+    return PyDateTimeAPI.DateTime_FromTimestamp(
+        <object> PyDateTimeAPI.DateTimeType,
+        (timestamp,) if tz is None else (timestamp, tz),
+        <object> NULL,
+    )
diff --git a/asynctnt/iproto/protocol.pxd b/asynctnt/iproto/protocol.pxd
index ac1dab9..20e283e 100644
--- a/asynctnt/iproto/protocol.pxd
+++ b/asynctnt/iproto/protocol.pxd
@@ -4,19 +4,46 @@ cimport asynctnt.iproto.tarantool as tarantool
 include "const.pxi"
 
 include "cmsgpuck.pxd"
+include "xd.pxd"
 include "python.pxd"
+include "bit.pxd"
 
 include "unicodeutil.pxd"
 include "schema.pxd"
+include "ext.pxd"
 include "buffer.pxd"
 include "rbuffer.pxd"
-include "request.pxd"
+
+include "requests/base.pxd"
+include "requests/ping.pxd"
+include "requests/call.pxd"
+include "requests/eval.pxd"
+include "requests/select.pxd"
+include "requests/insert.pxd"
+include "requests/delete.pxd"
+include "requests/update.pxd"
+include "requests/upsert.pxd"
+include "requests/prepare.pxd"
+include "requests/execute.pxd"
+include "requests/id.pxd"
+include "requests/auth.pxd"
+include "requests/streams.pxd"
+
 include "response.pxd"
 include "db.pxd"
 include "push.pxd"
 
 include "coreproto.pxd"
 
+cdef enum PostConnectionState:
+    POST_CONNECTION_NONE = 0
+    POST_CONNECTION_ID = 10
+    POST_CONNECTION_AUTH = 20
+    POST_CONNECTION_SCHEMA = 30
+    POST_CONNECTION_DONE = 100
+
+
+ctypedef object (*req_execute_func)(BaseProtocol, BaseRequest, float)
 
 cdef class BaseProtocol(CoreProtocol):
     cdef:
@@ -26,6 +53,7 @@ cdef class BaseProtocol(CoreProtocol):
         bint fetch_schema
         bint auto_refetch_schema
         float request_timeout
+        int post_con_state
 
         object connected_fut
         object on_connection_made_cb
@@ -41,19 +69,24 @@ cdef class BaseProtocol(CoreProtocol):
         bint _schema_fetch_in_progress
         object _refetch_schema_future
         Db _db
+        req_execute_func execute
 
         object create_future
 
     cdef void _set_connection_ready(self)
     cdef void _set_connection_error(self, e)
+    cdef void _post_con_state_machine(self)
 
+    cdef void _do_id(self)
     cdef void _do_auth(self, str username, str password)
     cdef void _do_fetch_schema(self, object fut)
     cdef object _refetch_schema(self)
 
     cdef inline uint64_t next_sync(self)
+    cdef inline uint64_t next_stream_id(self)
     cdef uint32_t transform_iterator(self, iterator) except *
 
-    cdef object _new_waiter_for_request(self, Request req, float timeout)
-    cdef Db _create_db(self)
-    cdef object execute(self, Request req, WriteBuffer buf, float timeout)
+    cdef object _new_waiter_for_request(self, Response response, BaseRequest req, float timeout)
+    cdef Db _create_db(self, bint gen_stream_id)
+    cdef object _execute_bad(self, BaseRequest req, float timeout)
+    cdef object _execute_normal(self, BaseRequest req, float timeout)
diff --git a/asynctnt/iproto/protocol.pyi b/asynctnt/iproto/protocol.pyi
new file mode 100644
index 0000000..120666b
--- /dev/null
+++ b/asynctnt/iproto/protocol.pyi
@@ -0,0 +1,245 @@
+import asyncio
+from typing import Iterator, Tuple, Optional, List, Any, Union, Dict
+
+
+class Field:
+    name: Optional[str]
+    """ Field name """
+
+    type: Optional[str]
+    """ Field type """
+
+    collation: Optional[str]
+    """ Field collation value """
+
+    is_nullable: Optional[bool]
+    """ If field may be null """
+
+    is_autoincrement: Optional[bool]
+    """ Is Autoincrement """
+
+    span: Optional[str]
+
+
+class Metadata:
+    fields: List[Field]
+    """ List of fields """
+
+    name_id_map: Dict[str, int]
+    """ Mapping name -> id """
+
+
+class SchemaIndex:
+    iid: int
+    """ Index id """
+
+    sid: int
+    """ Space id """
+
+    name: Optional[str]
+    index_type: Optional[str]
+    unique: Optional[bool]
+    metadata: Optional[Metadata]
+
+
+class SchemaSpace:
+    sid: int
+    owner: int
+    name: Optional[str]
+    engine: Optional[str]
+    field_count: int
+    flags: Optional[Any]
+    metadata: Optional[Metadata]
+    indexes: Dict[Union[int, str], SchemaIndex]
+
+
+class Schema:
+    id: int
+    spaces: Dict[Union[str, int], SchemaSpace]
+
+
+class TarantoolTuple:
+    def __repr__(self) -> str: ...
+
+    def __index__(self, i: int) -> Any: ...
+
+    def __len__(self) -> int: ...
+
+    def __contains__(self, item: str) -> bool: ...
+
+    def __getitem__(self, item: Union[int, str, slice]) -> Any: ...
+
+    def keys(self) -> Iterator[str]: ...
+
+    def values(self) -> Iterator[Any]: ...
+
+    def items(self) -> Iterator[Tuple[str, Any]]: ...
+
+    def get(self, item: str) -> Optional[Any]: ...
+
+    def __iter__(self): ...
+
+    def __next__(self): ...
+
+
+class IProtoErrorStackFrame:
+    error_type: str
+    file: str
+    line: int
+    message: str
+    err_no: int
+    code: int
+    fields: Dict[str, Any]
+
+
+class IProtoError:
+    trace: List[IProtoErrorStackFrame]
+
+
+BodyItem = Union[TarantoolTuple, List[Any], Dict[Any, Any], Any]
+
+
+class Response:
+    errmsg: Optional[str]
+    error: Optional[IProtoError]
+    encoding: bytes
+    autoincrement_ids: Optional[List[int]]
+    body: Optional[List[BodyItem]]
+    metadata: Optional[Metadata]
+    params: Optional[Metadata]
+    params_count: int
+
+    @property
+    def sync(self) -> int: ...
+
+    @property
+    def code(self) -> int: ...
+
+    @property
+    def return_code(self) -> int: ...
+
+    @property
+    def schema_id(self) -> int: ...
+
+    @property
+    def stmt_id(self) -> int: ...
+
+    @property
+    def rowcount(self) -> int: ...
+
+    def done(self) -> bool: ...
+
+    def __len__(self) -> int: ...
+
+    def __getitem__(self, i) -> BodyItem: ...
+
+    def __iter__(self): ...
+
+
+class PushIterator:
+    def __init__(self, fut: asyncio.Future): ...
+
+    def __iter__(self): ...
+
+    def __next__(self): ...
+
+    def __aiter__(self): ...
+
+    async def __anext__(self): ...
+
+    @property
+    def response(self) -> Response: ...
+
+
+class Db:
+    @property
+    def stream_id(self) -> int: ...
+
+    def set_stream_id(self, stream_id: int): ...
+
+    def ping(self, timeout: float = -1): ...
+
+    def call16(self,
+               func_name: str,
+               args=None,
+               timeout: float = -1,
+               push_subscribe: bool = False): ...
+
+    def call(self,
+             func_name: str,
+             args=None,
+             timeout: float = -1,
+             push_subscribe: bool = False): ...
+
+    def eval(self,
+             expression: str,
+             args=None,
+             timeout: float = -1,
+             push_subscribe: bool = False): ...
+
+    def select(self,
+               space, key=None,
+               offset: int = 0,
+               limit: int = 0xffffffff,
+               index=0,
+               iterator=0,
+               timeout: float = -1,
+               check_schema_change: bool = True): ...
+
+    def insert(self,
+               space, t,
+               replace: bool = False,
+               timeout: float = -1): ...
+
+    def replace(self, space, t,
+                timeout: float = -1): ...
+
+    def delete(self, space, key, index=0,
+               timeout: float = -1): ...
+
+    def update(self,
+               space, key, operations, index=0,
+               timeout: float = -1): ...
+
+    def upsert(self,
+               space, t, operations,
+               timeout: float = -1): ...
+
+    def execute(self,
+                query, args,
+                parse_metadata: bool = True,
+                timeout: float = -1): ...
+
+    def prepare(self,
+                query,
+                parse_metadata: bool = True,
+                timeout: float = -1): ...
+
+    def begin(self,
+              isolation: int,
+              tx_timeout: float,
+              timeout: float = -1): ...
+
+    def commit(self, timeout: float = -1): ...
+
+    def rollback(self, timeout: float = -1): ...
+
+
+class Protocol:
+    @property
+    def schema_id(self) -> int: ...
+
+    @property
+    def schema(self) -> Schema: ...
+
+    def create_db(self, gen_stream_id: bool = False) -> Db: ...
+
+    def get_common_db(self) -> Db: ...
+
+    def refetch_schema(self) -> asyncio.Future: ...
+
+    def is_connected(self) -> bool: ...
+
+    def is_fully_connected(self) -> bool: ...
+
+    def get_version(self) -> tuple: ...
diff --git a/asynctnt/iproto/protocol.pyx b/asynctnt/iproto/protocol.pyx
index e32a807..7798879 100644
--- a/asynctnt/iproto/protocol.pyx
+++ b/asynctnt/iproto/protocol.pyx
@@ -1,6 +1,8 @@
 # cython: language_level=3
 
 cimport cpython.dict
+from cpython.datetime cimport import_datetime
+import_datetime()
 
 import asyncio
 import enum
@@ -11,9 +13,26 @@ include "const.pxi"
 
 include "unicodeutil.pyx"
 include "schema.pyx"
+include "ext.pyx"
 include "buffer.pyx"
 include "rbuffer.pyx"
-include "request.pyx"
+
+include "requests/base.pyx"
+include "requests/ping.pyx"
+include "requests/call.pyx"
+include "requests/eval.pyx"
+include "requests/select.pyx"
+include "requests/insert.pyx"
+include "requests/delete.pyx"
+include "requests/update.pyx"
+include "requests/upsert.pyx"
+include "requests/prepare.pyx"
+include "requests/execute.pyx"
+include "requests/id.pyx"
+include "requests/auth.pyx"
+include "requests/streams.pyx"
+
+include "ttuple.pyx"
 include "response.pyx"
 include "db.pyx"
 include "push.pyx"
@@ -60,6 +79,7 @@ cdef class BaseProtocol(CoreProtocol):
         self.fetch_schema = fetch_schema
         self.auto_refetch_schema = auto_refetch_schema
         self.request_timeout = request_timeout or 0
+        self.post_con_state = POST_CONNECTION_NONE
 
         self.connected_fut = connected_fut
         self.on_connection_made_cb = on_connection_made
@@ -71,11 +91,13 @@ cdef class BaseProtocol(CoreProtocol):
 
         self._reqs = {}
         self._sync = 0
+        self._last_stream_id = 0
         self._schema_id = -1
         self._schema = Schema.__new__(Schema, self._schema_id)
         self._schema_fetch_in_progress = False
         self._refetch_schema_future = None
-        self._db = self._create_db()
+        self._db = self._create_db(<bint> False)
+        self.execute = self._execute_bad
 
         try:
             self.create_future = self.loop.create_future
@@ -102,19 +124,51 @@ cdef class BaseProtocol(CoreProtocol):
         if not self.connected_fut.done():
             self.connected_fut.set_exception(e)
             self.con_state = CONNECTION_BAD
+            self.post_con_state = POST_CONNECTION_NONE
+            self.execute = self._execute_bad
 
     cdef void _on_greeting_received(self):
-        if self.username and self.password:
-            self._do_auth(self.username, self.password)
-        elif self.fetch_schema:
-            self._refetch_schema()
-        else:
+        self.post_con_state = POST_CONNECTION_ID
+        self.execute = self._execute_normal
+        self._post_con_state_machine()
+
+    cdef void _post_con_state_machine(self):
+        if self.post_con_state == POST_CONNECTION_ID:
+            assert self.version is not None
+            if self.version >= (2, 10, 0):
+                # send <id> request
+                self._do_id()
+                return
+            else:
+                # Tarantool does not support id call - call auth request
+                self.post_con_state = POST_CONNECTION_AUTH
+
+        if self.post_con_state == POST_CONNECTION_AUTH:
+            if self.username and self.password:
+                # send <auth> request
+                self._do_auth(self.username, self.password)
+                return
+            else:
+                # no need to auth - fetch schema
+                self.post_con_state = POST_CONNECTION_SCHEMA
+
+        if self.post_con_state == POST_CONNECTION_SCHEMA:
+            if self.fetch_schema:
+                self._refetch_schema()
+                return
+            else:
+                # nothing else to do - connection is done
+                self.post_con_state = POST_CONNECTION_DONE
+
+        if self.post_con_state == POST_CONNECTION_DONE:
             self._set_connection_ready()
+            return
 
     cdef void _on_response_received(self, const char *buf, uint32_t buf_len):
         cdef:
             PyObject *req_p
-            Request req
+            Response response
+            BaseRequest req
             Header hdr
             bint is_chunk
             object waiter
@@ -129,19 +183,20 @@ cdef class BaseProtocol(CoreProtocol):
 
         sync_obj = <object> hdr.sync
 
-        req_p = cpython.dict.PyDict_GetItem(self._reqs, sync_obj)
-        if req_p is NULL:
+        response_p = cpython.dict.PyDict_GetItem(self._reqs, sync_obj)
+        if response_p is NULL:
             logger.warning('sync %d not found', hdr.sync)
             return
 
         is_chunk = (hdr.code == tarantool.IPROTO_CHUNK)
 
-        req = <Request> req_p
-        req.response._sync = hdr.sync
-        req.response._schema_id = hdr.schema_id
+        response = <Response> response_p
+        req = response.request_
+        response.sync_ = hdr.sync
+        response.schema_id_ = hdr.schema_id
         if not is_chunk:
-            req.response._code = hdr.code
-            req.response._return_code = hdr.return_code
+            response.code_ = hdr.code
+            response.return_code_ = hdr.return_code
             cpython.dict.PyDict_DelItem(self._reqs, sync_obj)
         else:
             if not req.push_subscribe:
@@ -153,7 +208,7 @@ cdef class BaseProtocol(CoreProtocol):
         if buf != &buf[buf_len]:
             # has body
             try:
-                response_parse_body(buf, buf_len, req.response, req, is_chunk)
+                response_parse_body(buf, buf_len, response, req, is_chunk)
             except Exception as e:
                 err = e
 
@@ -161,8 +216,8 @@ cdef class BaseProtocol(CoreProtocol):
         if self.con_state == CONNECTION_FULL \
                 and req.check_schema_change \
                 and self.auto_refetch_schema \
-                and req.response._schema_id > 0 \
-                and req.response._schema_id != self._schema_id:
+                and response.schema_id_ > 0 \
+                and response.schema_id_ != self._schema_id:
             self._refetch_schema()
 
         # returning result
@@ -174,22 +229,44 @@ cdef class BaseProtocol(CoreProtocol):
             return
 
         if err is not None:
-            req.response.set_exception(err)
+            response.set_exception(err)
             waiter.set_exception(err)
             return
 
-        if req.response.is_error():
-            err = TarantoolDatabaseError(req.response._return_code,
-                                         req.response._errmsg)
-            req.response.set_exception(err)
+        if response.is_error():
+            err = TarantoolDatabaseError(response.return_code_,
+                                         response.errmsg,
+                                         response.error)
+            response.set_exception(err)
             waiter.set_exception(err)
             return
 
-        waiter.set_result(req.response)
+        waiter.set_result(response)
+
+    cdef void _do_id(self):
+        fut = self._db._id(0.0)
+
+        def on_id(f):
+            if f.cancelled():
+                self._set_connection_error(asyncio.futures.CancelledError())
+                return
+            e = f.exception()
+            if not e:
+                logger.debug('Tarantool[%s:%s] identified successfully',
+                             self.host, self.port)
+
+                self.post_con_state = POST_CONNECTION_AUTH
+                self._post_con_state_machine()
+            else:
+                logger.error('Tarantool[%s:%s] identification failed: %s',
+                             self.host, self.port, str(e))
+                self._set_connection_error(e)
+
+        fut.add_done_callback(on_id)
 
     cdef void _do_auth(self, str username, str password):
         # No extra error handling from Db.execute
-        fut = self._db._auth(self.salt, username, password, 0, False, False)
+        fut = self._db._auth(self.salt, username, password, 0)
 
         def on_authorized(f):
             if f.cancelled():
@@ -200,10 +277,8 @@ cdef class BaseProtocol(CoreProtocol):
                 logger.debug('Tarantool[%s:%s] Authorized successfully',
                              self.host, self.port)
 
-                if self.fetch_schema:
-                    self._do_fetch_schema(None)
-                else:
-                    self._set_connection_ready()
+                self.post_con_state = POST_CONNECTION_SCHEMA
+                self._post_con_state_machine()
             else:
                 logger.error('Tarantool[%s:%s] Authorization failed: %s',
                              self.host, self.port, str(e))
@@ -252,7 +327,8 @@ cdef class BaseProtocol(CoreProtocol):
                     return
 
                 self._schema_id = self._schema.id
-                self._set_connection_ready()
+                self.post_con_state = POST_CONNECTION_DONE
+                self._post_con_state_machine()
                 if fut is not None and not fut.done():
                     fut.set_result(self._schema)
             else:
@@ -285,7 +361,8 @@ cdef class BaseProtocol(CoreProtocol):
 
     cdef void _on_connection_lost(self, exc):
         cdef:
-            Request req
+            BaseRequest req
+            Response response
             PyObject *pkey
             PyObject *pvalue
             object key, value
@@ -295,11 +372,14 @@ cdef class BaseProtocol(CoreProtocol):
             return
 
         self._closing = True
+        self.post_con_state = POST_CONNECTION_NONE
+        self.execute = self._execute_bad
 
         pos = 0
         while cpython.dict.PyDict_Next(self._reqs, &pos, &pkey, &pvalue):
             sync = <uint64_t> <object> pkey
-            req = <Request> pvalue
+            response = <Response> pvalue
+            req = response.request_
 
             waiter = req.waiter
             if waiter and not waiter.done():
@@ -325,8 +405,8 @@ cdef class BaseProtocol(CoreProtocol):
 
                 if err is not None:
                     waiter.set_exception(err)
-                    if req.response is not None:
-                        req.response.set_exception(err)
+                    if response is not None:
+                        response.set_exception(err)
 
         if self.on_connection_lost_cb:
             self.on_connection_lost_cb(exc)
@@ -337,13 +417,20 @@ cdef class BaseProtocol(CoreProtocol):
         self._sync += 1
         return self._sync
 
+    cdef inline uint64_t next_stream_id(self):
+        self._last_stream_id += 1
+        return self._last_stream_id
+
     def _on_request_timeout(self, waiter):
-        cdef Request req
+        cdef:
+            BaseRequest req
+            Response response
 
         if waiter.done():
             return
 
-        req = waiter._req
+        response = waiter._response
+        req = response.request_
         req.timeout_handle.cancel()
         req.timeout_handle = None
         waiter.set_exception(
@@ -352,19 +439,17 @@ cdef class BaseProtocol(CoreProtocol):
         )
 
     def _on_request_completed(self, fut):
-        cdef Request req = fut._req
-        fut._req = None
+        cdef BaseRequest req = (<Response> fut._response).request_
+        fut._response = None
 
         if req.timeout_handle is not None:
             req.timeout_handle.cancel()
             req.timeout_handle = None
 
-    cdef object _new_waiter_for_request(self, Request req, float timeout):
+    cdef object _new_waiter_for_request(self, Response response, BaseRequest req, float timeout):
         fut = self.create_future()
-        fut._req = req  # to be able to retrieve request after done()
         req.waiter = fut
-        req.response = Response.__new__(Response, self.encoding,
-                                        req.push_subscribe)
+        fut._response = response  # to be able to retrieve request after done()
 
         if timeout < 0:
             timeout = self.request_timeout
@@ -375,23 +460,35 @@ cdef class BaseProtocol(CoreProtocol):
             fut.add_done_callback(self._on_request_completed_cb)
         return fut
 
-    cdef Db _create_db(self):
-        return Db.new(self)
+    cdef Db _create_db(self, bint gen_stream_id):
+        cdef uint64_t stream_id
+        if gen_stream_id:
+            stream_id = self.next_stream_id()
+        else:
+            stream_id = 0
+
+        return Db.create(self, stream_id)
 
-    def create_db(self):
-        return self._create_db()
+    def create_db(self, bint gen_stream_id = False):
+        return self._create_db(gen_stream_id)
 
     def get_common_db(self):
         return self._db
 
-    cdef object execute(self, Request req, WriteBuffer buf, float timeout):
-        if self.con_state == CONNECTION_BAD:
-            raise TarantoolNotConnectedError('Tarantool is not connected')
+    cdef object _execute_bad(self, BaseRequest req, float timeout):
+        raise TarantoolNotConnectedError('Tarantool is not connected')
 
-        cpython.dict.PyDict_SetItem(self._reqs, req.sync, req)
-        self._write(buf)
+    cdef object _execute_normal(self, BaseRequest req, float timeout):
+        cdef Response response
+        response = <Response> Response.__new__(Response)
+        response.request_ = req
+        response.encoding = self.encoding
+        if req.push_subscribe:
+            response.init_push()
+        cpython.dict.PyDict_SetItem(self._reqs, req.sync, response)
+        self._write(req.encode(self.encoding))
 
-        return self._new_waiter_for_request(req, timeout)
+        return self._new_waiter_for_request(response, req, timeout)
 
     cdef uint32_t transform_iterator(self, iterator) except *:
         if isinstance(iterator, int):
@@ -421,5 +518,3 @@ cdef class BaseProtocol(CoreProtocol):
 class Protocol(BaseProtocol, asyncio.Protocol):
     pass
 
-
-TarantoolTuple = <object> tupleobj.AtntTuple_InitTypes()
diff --git a/asynctnt/iproto/push.pxd b/asynctnt/iproto/push.pxd
index ceff0a7..1b1003c 100644
--- a/asynctnt/iproto/push.pxd
+++ b/asynctnt/iproto/push.pxd
@@ -1,6 +1,8 @@
+cimport cython
 
+@cython.final
 cdef class PushIterator:
     cdef:
         object _fut
-        Request _request
+        BaseRequest _request
         Response _response
diff --git a/asynctnt/iproto/push.pyx b/asynctnt/iproto/push.pyx
index d640b78..a53f87e 100644
--- a/asynctnt/iproto/push.pyx
+++ b/asynctnt/iproto/push.pyx
@@ -30,13 +30,15 @@ cdef class PushIterator:
             :type fut: asyncio.Future
         """
         cdef:
-            Request request
-        if not hasattr(fut, '_req'):
+            Response response
+            BaseRequest request
+        if not hasattr(fut, '_response'):
             raise ValueError('Future is invalid. Make sure to call with '
                              'a future returned from a method with '
                              'push_subscribe=True flag')
 
-        request = <Request>fut._req
+        response = <Response> fut._response
+        request = response.request_
 
         if not request.push_subscribe:
             raise ValueError('Future is invalid. Make sure to call with '
@@ -45,7 +47,7 @@ cdef class PushIterator:
 
         self._fut = fut
         self._request = request
-        self._response = request.response
+        self._response = response
 
     def __iter__(self):
         raise RuntimeError('Cannot use iter with PushIterator - use aiter')
@@ -61,7 +63,7 @@ cdef class PushIterator:
         cdef Response response
         response = self._response
 
-        if response.push_len() == 0 and response._code >= 0:
+        if response.push_len() == 0 and response.code_ >= 0:
             # no more data left
             raise StopAsyncIteration
 
@@ -85,7 +87,7 @@ cdef class PushIterator:
         if response.push_len() > 0:
             return response.pop_push()
 
-        if response._code >= 0:
+        if response.code_ >= 0:
             ev.clear()
             raise StopAsyncIteration
 
diff --git a/asynctnt/iproto/python.pxd b/asynctnt/iproto/python.pxd
index ab6461c..ffd6fa7 100644
--- a/asynctnt/iproto/python.pxd
+++ b/asynctnt/iproto/python.pxd
@@ -1,3 +1,76 @@
+from cpython.version cimport PY_VERSION_HEX
+
 cdef extern from "Python.h":
     char *PyByteArray_AS_STRING(object obj)
     int Py_REFCNT(object obj)
+
+
+cdef extern from "datetime.h":
+    """
+    /* Backport for Python 2.x */
+    #if PY_MAJOR_VERSION < 3
+        #ifndef PyDateTime_DELTA_GET_DAYS
+            #define PyDateTime_DELTA_GET_DAYS(o) (((PyDateTime_Delta*)o)->days)
+        #endif
+        #ifndef PyDateTime_DELTA_GET_SECONDS
+            #define PyDateTime_DELTA_GET_SECONDS(o) (((PyDateTime_Delta*)o)->seconds)
+        #endif
+        #ifndef PyDateTime_DELTA_GET_MICROSECONDS
+            #define PyDateTime_DELTA_GET_MICROSECONDS(o) (((PyDateTime_Delta*)o)->microseconds)
+        #endif
+    #endif
+    /* Backport for Python < 3.6 */
+    #if PY_VERSION_HEX < 0x030600a4
+        #ifndef PyDateTime_TIME_GET_FOLD
+            #define PyDateTime_TIME_GET_FOLD(o) ((void)(o), 0)
+        #endif
+        #ifndef PyDateTime_DATE_GET_FOLD
+            #define PyDateTime_DATE_GET_FOLD(o) ((void)(o), 0)
+        #endif
+    #endif
+    /* Backport for Python < 3.6 */
+    #if PY_VERSION_HEX < 0x030600a4
+        #define __Pyx_DateTime_DateTimeWithFold(year, month, day, hour, minute, second, microsecond, tz, fold) \
+            ((void)(fold), PyDateTimeAPI->DateTime_FromDateAndTime(year, month, day, hour, minute, second, \
+                microsecond, tz, PyDateTimeAPI->DateTimeType))
+        #define __Pyx_DateTime_TimeWithFold(hour, minute, second, microsecond, tz, fold) \
+            ((void)(fold), PyDateTimeAPI->Time_FromTime(hour, minute, second, microsecond, tz, PyDateTimeAPI->TimeType))
+    #else /* For Python 3.6+ so that we can pass tz */
+        #define __Pyx_DateTime_DateTimeWithFold(year, month, day, hour, minute, second, microsecond, tz, fold) \
+            PyDateTimeAPI->DateTime_FromDateAndTimeAndFold(year, month, day, hour, minute, second, \
+                microsecond, tz, fold, PyDateTimeAPI->DateTimeType)
+        #define __Pyx_DateTime_TimeWithFold(hour, minute, second, microsecond, tz, fold) \
+            PyDateTimeAPI->Time_FromTimeAndFold(hour, minute, second, microsecond, tz, fold, PyDateTimeAPI->TimeType)
+    #endif
+    /* Backport for Python < 3.7 */
+    #if PY_VERSION_HEX < 0x030700b1
+        #define __Pyx_TimeZone_UTC NULL
+        #define __Pyx_TimeZone_FromOffset(offset) ((void)(offset), (PyObject*)NULL)
+        #define __Pyx_TimeZone_FromOffsetAndName(offset, name) ((void)(offset), (void)(name), (PyObject*)NULL)
+    #else
+        #define __Pyx_TimeZone_UTC PyDateTime_TimeZone_UTC
+        #define __Pyx_TimeZone_FromOffset(offset) PyTimeZone_FromOffset(offset)
+        #define __Pyx_TimeZone_FromOffsetAndName(offset, name) PyTimeZone_FromOffsetAndName(offset, name)
+    #endif
+    /* Backport for Python < 3.10 */
+    #if PY_VERSION_HEX < 0x030a00a1
+        #ifndef PyDateTime_TIME_GET_TZINFO
+            #define PyDateTime_TIME_GET_TZINFO(o) \
+                ((((PyDateTime_Time*)o)->hastzinfo) ? ((PyDateTime_Time*)o)->tzinfo : Py_None)
+        #endif
+        #ifndef PyDateTime_DATE_GET_TZINFO
+            #define PyDateTime_DATE_GET_TZINFO(o) \
+                ((((PyDateTime_DateTime*)o)->hastzinfo) ? ((PyDateTime_DateTime*)o)->tzinfo : Py_None)
+        #endif
+    #endif
+    """
+
+    # The above macros is Python 3.7+ so we use these instead
+    object __Pyx_TimeZone_FromOffset(object offset)
+
+
+cdef inline object timezone_new(object offset):
+    if PY_VERSION_HEX < 0x030700b1:
+        from datetime import timezone
+        return timezone(offset)
+    return __Pyx_TimeZone_FromOffset(offset)
diff --git a/asynctnt/iproto/rbuffer.pxd b/asynctnt/iproto/rbuffer.pxd
index a96e3d3..02510ee 100644
--- a/asynctnt/iproto/rbuffer.pxd
+++ b/asynctnt/iproto/rbuffer.pxd
@@ -1,12 +1,11 @@
+cimport cython
 from libc.stdint cimport uint32_t
 
-
 cdef inline size_t size_t_max(size_t a, size_t b):
     if a > b:
         return a
     return b
 
-
 cdef inline uint32_t nearest_power_of_2(uint32_t v):
     v -= 1
     v |= v >> 1
@@ -17,7 +16,7 @@ cdef inline uint32_t nearest_power_of_2(uint32_t v):
     v += 1
     return v
 
-
+@cython.final
 cdef class ReadBuffer:
     cdef:
         char *buf
@@ -28,7 +27,7 @@ cdef class ReadBuffer:
         str encoding
 
     @staticmethod
-    cdef ReadBuffer new(str encoding, size_t initial_buffer_size=*)
+    cdef ReadBuffer create(str encoding, size_t initial_buffer_size= *)
 
     cdef void _reallocate(self, size_t new_size) except *
     cdef int extend(self, const char *data, size_t len) except -1
diff --git a/asynctnt/iproto/rbuffer.pyx b/asynctnt/iproto/rbuffer.pyx
index 2a66df9..56d9a1f 100644
--- a/asynctnt/iproto/rbuffer.pyx
+++ b/asynctnt/iproto/rbuffer.pyx
@@ -16,7 +16,7 @@ cdef class ReadBuffer:
         self.encoding = None
 
     @staticmethod
-    cdef ReadBuffer new(str encoding, size_t initial_buffer_size=0x80000):
+    cdef ReadBuffer create(str encoding, size_t initial_buffer_size=0x80000):
         cdef ReadBuffer b
         b = ReadBuffer.__new__(ReadBuffer)
 
diff --git a/asynctnt/iproto/request.pxd b/asynctnt/iproto/request.pxd
deleted file mode 100644
index c5f5b11..0000000
--- a/asynctnt/iproto/request.pxd
+++ /dev/null
@@ -1,27 +0,0 @@
-from libc.stdint cimport uint32_t, uint64_t, int64_t
-
-
-cdef class Request:
-    cdef:
-        tarantool.iproto_type op
-        uint64_t sync
-        int64_t schema_id
-        SchemaSpace space
-        object waiter
-        object timeout_handle
-        bint parse_metadata
-        bint parse_as_tuples
-        bint push_subscribe
-        bint check_schema_change
-        Response response
-
-    @staticmethod
-    cdef inline Request new(tarantool.iproto_type op,
-                            uint64_t sync, int64_t schema_id,
-                            SchemaSpace space, bint push_subscribe,
-                            bint check_schema_change)
-
-    cdef inline TntFields fields(self):
-        if self.space is None:
-            return None
-        return self.space.fields
diff --git a/asynctnt/iproto/request.pyx b/asynctnt/iproto/request.pyx
deleted file mode 100644
index 31a9a8c..0000000
--- a/asynctnt/iproto/request.pyx
+++ /dev/null
@@ -1,35 +0,0 @@
-cimport cython
-from libc.stdint cimport uint64_t, int64_t
-
-
-@cython.final
-@cython.freelist(REQUEST_FREELIST)
-cdef class Request:
-    @staticmethod
-    cdef inline Request new(tarantool.iproto_type op,
-                            uint64_t sync, int64_t schema_id,
-                            SchemaSpace space, bint push_subscribe,
-                            bint check_schema_change):
-        cdef Request req
-        req = Request.__new__(Request)
-        req.op = op
-        req.sync = sync
-        req.schema_id = schema_id
-        req.space = space
-        req.waiter = None
-        req.timeout_handle = None
-        req.parse_as_tuples = space is not None
-        req.parse_metadata = True
-        req.push_subscribe = push_subscribe
-        req.response = None
-        req.check_schema_change = check_schema_change
-        return req
-
-    def __repr__(self):  # pragma: nocover
-        return \
-            '<Request op={} sync={} schema_id={} push_subscribe={}>'.format(
-                self.op,
-                self.sync,
-                self.schema_id,
-                self.push_subscribe
-            )
diff --git a/asynctnt/iproto/requests/auth.pxd b/asynctnt/iproto/requests/auth.pxd
new file mode 100644
index 0000000..a726821
--- /dev/null
+++ b/asynctnt/iproto/requests/auth.pxd
@@ -0,0 +1,5 @@
+cdef class AuthRequest(BaseRequest):
+    cdef:
+        bytes salt
+        str username
+        str password
diff --git a/asynctnt/iproto/requests/auth.pyx b/asynctnt/iproto/requests/auth.pyx
new file mode 100644
index 0000000..5598ad8
--- /dev/null
+++ b/asynctnt/iproto/requests/auth.pyx
@@ -0,0 +1,84 @@
+cimport cython
+cimport cpython
+cimport cpython.bytes
+
+import hashlib
+
+cdef inline bytes _sha1(tuple values):
+    cdef object sha = hashlib.sha1()
+    for i in values:
+        if i is not None:
+            sha.update(i)
+    return sha.digest()
+
+cdef inline bytes _strxor(bytes hash1, bytes scramble):
+    cdef:
+        char *hash1_str
+        ssize_t hash1_len
+
+        char *scramble_str
+        ssize_t scramble_len
+    cpython.bytes.PyBytes_AsStringAndSize(hash1,
+                                          &hash1_str, &hash1_len)
+    cpython.bytes.PyBytes_AsStringAndSize(scramble,
+                                          &scramble_str, &scramble_len)
+    for i in range(scramble_len):
+        scramble_str[i] = hash1_str[i] ^ scramble_str[i]
+    return scramble
+
+@cython.final
+cdef class AuthRequest(BaseRequest):
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        cdef:
+            char *begin
+            char *p
+            uint32_t body_map_sz
+            uint32_t max_body_len
+
+            char *username_str
+            ssize_t username_len
+
+            char *scramble_str
+            ssize_t scramble_len
+
+        username_bytes = encode_unicode_string(self.username, buffer._encoding)
+        password_bytes = encode_unicode_string(self.password, buffer._encoding)
+
+        hash1 = _sha1((password_bytes,))
+        hash2 = _sha1((hash1,))
+        scramble = _sha1((self.salt, hash2))
+        scramble = _strxor(hash1, scramble)
+
+        cpython.bytes.PyBytes_AsStringAndSize(username_bytes,
+                                              &username_str, &username_len)
+        cpython.bytes.PyBytes_AsStringAndSize(scramble,
+                                              &scramble_str, &scramble_len)
+        body_map_sz = 2
+        # Size description:
+        # mp_sizeof_map()
+        # + mp_sizeof_uint(TP_USERNAME)
+        # + mp_sizeof_str(username_len)
+        # + mp_sizeof_uint(TP_TUPLE)
+        # + mp_sizeof_array(2)
+        # + mp_sizeof_str(9) (chap-sha1)
+        # + mp_sizeof_str(SCRAMBLE_SIZE)
+        max_body_len = 1 \
+                       + 1 \
+                       + mp_sizeof_str(<uint32_t> username_len) \
+                       + 1 \
+                       + 1 \
+                       + 1 + 9 \
+                       + mp_sizeof_str(<uint32_t> scramble_len)
+
+        buffer.ensure_allocated(max_body_len)
+
+        p = begin = &buffer._buf[buffer._length]
+        p = mp_encode_map(p, body_map_sz)
+        p = mp_encode_uint(p, tarantool.IPROTO_USER_NAME)
+        p = mp_encode_str(p, username_str, <uint32_t> username_len)
+
+        p = mp_encode_uint(p, tarantool.IPROTO_TUPLE)
+        p = mp_encode_array(p, 2)
+        p = mp_encode_str(p, "chap-sha1", 9)
+        p = mp_encode_str(p, scramble_str, <uint32_t> scramble_len)
+        buffer._length += (p - begin)
diff --git a/asynctnt/iproto/requests/base.pxd b/asynctnt/iproto/requests/base.pxd
new file mode 100644
index 0000000..544fb58
--- /dev/null
+++ b/asynctnt/iproto/requests/base.pxd
@@ -0,0 +1,30 @@
+from libc.stdint cimport uint64_t, int64_t
+
+
+cdef class BaseRequest:
+    cdef:
+        tarantool.iproto_type op
+        uint64_t sync
+        int64_t schema_id
+        uint64_t stream_id
+        SchemaSpace space
+        object waiter
+        object timeout_handle
+        bint parse_metadata
+        bint parse_as_tuples
+        bint push_subscribe
+        bint check_schema_change
+
+    cdef inline Metadata metadata(self):
+        if self.space is None:
+            return None
+        return self.space.metadata
+
+    cdef inline WriteBuffer encode(self, bytes encoding)
+    cdef int encode_body(self, WriteBuffer buffer) except -1
+
+
+cdef char *encode_key_sequence(WriteBuffer buffer,
+                               char *p, object t,
+                               Metadata metadata,
+                               bint default_none) except NULL
diff --git a/asynctnt/iproto/requests/base.pyx b/asynctnt/iproto/requests/base.pyx
new file mode 100644
index 0000000..e917867
--- /dev/null
+++ b/asynctnt/iproto/requests/base.pyx
@@ -0,0 +1,54 @@
+cimport cython
+from libc.stdint cimport uint64_t, int64_t
+
+
+@cython.freelist(REQUEST_FREELIST)
+cdef class BaseRequest:
+
+    # def __cinit__(self):
+    #     self.sync = 0
+    #     self.schema_id = -1
+    #     self.parse_as_tuples = False
+    #     self.parse_metadata = True
+    #     self.push_subscribe = False
+
+    cdef inline WriteBuffer encode(self, bytes encoding):
+        cdef WriteBuffer buffer = WriteBuffer.create(encoding)
+        buffer.write_header(self.sync, self.op, self.schema_id, self.stream_id)
+        self.encode_body(buffer)
+        buffer.write_length()
+        return buffer
+
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        return 0
+
+    def __repr__(self):  # pragma: nocover
+        return \
+            '<Request op={} sync={} schema_id={} push_subscribe={}>'.format(
+                self.op,
+                self.sync,
+                self.schema_id,
+                self.push_subscribe
+            )
+
+
+cdef char *encode_key_sequence(WriteBuffer buffer,
+                               char *p, object t,
+                               Metadata metadata,
+                               bint default_none) except NULL:
+    if isinstance(t, list) or t is None:
+        return buffer.mp_encode_list(p, <list> t)
+    elif isinstance(t, tuple):
+        return buffer.mp_encode_tuple(p, <tuple> t)
+    elif isinstance(t, dict) and metadata is not None:
+        return buffer.mp_encode_list(
+            p, dict_to_list_fields(<dict> t, metadata, default_none)
+        )
+    else:
+        if metadata is not None:
+            msg = 'sequence must be either list, tuple or dict'
+        else:
+            msg = 'sequence must be either list or tuple'
+        raise TypeError(
+            '{}, got: {}'.format(msg, type(t))
+        )
diff --git a/asynctnt/iproto/requests/call.pxd b/asynctnt/iproto/requests/call.pxd
new file mode 100644
index 0000000..0d9b62e
--- /dev/null
+++ b/asynctnt/iproto/requests/call.pxd
@@ -0,0 +1,4 @@
+cdef class CallRequest(BaseRequest):
+    cdef:
+        str func_name
+        object args
diff --git a/asynctnt/iproto/requests/call.pyx b/asynctnt/iproto/requests/call.pyx
new file mode 100644
index 0000000..852bb40
--- /dev/null
+++ b/asynctnt/iproto/requests/call.pyx
@@ -0,0 +1,44 @@
+cimport cython
+cimport asynctnt.iproto.tarantool as tarantool
+
+@cython.final
+cdef class CallRequest(BaseRequest):
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        cdef:
+            char *begin
+            char *p
+            uint32_t body_map_sz
+            uint32_t max_body_len
+
+            bytes func_name_temp
+            char *func_name_str
+            ssize_t func_name_len
+
+        func_name_str = NULL
+        func_name_len = 0
+
+        func_name_temp = encode_unicode_string(self.func_name, buffer._encoding)
+        cpython.bytes.PyBytes_AsStringAndSize(func_name_temp,
+                                              &func_name_str,
+                                              &func_name_len)
+        body_map_sz = 2
+        # Size description:
+        # mp_sizeof_map()
+        # + mp_sizeof_uint(TP_FUNCTION)
+        # + mp_sizeof_str(func_name)
+        # + mp_sizeof_uint(TP_TUPLE)
+        max_body_len = 1 \
+                       + 1 \
+                       + mp_sizeof_str(<uint32_t> func_name_len) \
+                       + 1
+
+        buffer.ensure_allocated(max_body_len)
+
+        p = begin = &buffer._buf[buffer._length]
+        p = mp_encode_map(p, body_map_sz)
+        p = mp_encode_uint(p, tarantool.IPROTO_FUNCTION_NAME)
+        p = mp_encode_str(p, func_name_str, <uint32_t> func_name_len)
+
+        p = mp_encode_uint(p, tarantool.IPROTO_TUPLE)
+        buffer._length += (p - begin)
+        p = encode_key_sequence(buffer, p, self.args, None, False)
diff --git a/asynctnt/iproto/requests/delete.pxd b/asynctnt/iproto/requests/delete.pxd
new file mode 100644
index 0000000..87534ce
--- /dev/null
+++ b/asynctnt/iproto/requests/delete.pxd
@@ -0,0 +1,4 @@
+cdef class DeleteRequest(BaseRequest):
+    cdef:
+        SchemaIndex index
+        object key
diff --git a/asynctnt/iproto/requests/delete.pyx b/asynctnt/iproto/requests/delete.pyx
new file mode 100644
index 0000000..2cc7b51
--- /dev/null
+++ b/asynctnt/iproto/requests/delete.pyx
@@ -0,0 +1,46 @@
+cimport cython
+
+@cython.final
+cdef class DeleteRequest(BaseRequest):
+
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        cdef:
+            char *p
+            char *begin
+            uint32_t body_map_sz
+            uint32_t max_body_len
+            uint32_t space_id, index_id
+
+        space_id = self.space.sid
+        index_id = self.index.iid
+
+        body_map_sz = 2 \
+                      + <uint32_t> (index_id > 0)
+        # Size description:
+        # mp_sizeof_map(body_map_sz)
+        # + mp_sizeof_uint(TP_SPACE)
+        # + mp_sizeof_uint(space)
+        max_body_len = 1 \
+                       + 1 \
+                       + 9
+
+        if index_id > 0:
+            # mp_sizeof_uint(TP_INDEX) + mp_sizeof_uint(index)
+            max_body_len += 1 + 9
+
+        max_body_len += 1  # mp_sizeof_uint(TP_KEY);
+
+        buffer.ensure_allocated(max_body_len)
+
+        p = begin = &buffer._buf[buffer._length]
+        p = mp_encode_map(p, body_map_sz)
+        p = mp_encode_uint(p, tarantool.IPROTO_SPACE_ID)
+        p = mp_encode_uint(p, space_id)
+
+        if index_id > 0:
+            p = mp_encode_uint(p, tarantool.IPROTO_INDEX_ID)
+            p = mp_encode_uint(p, index_id)
+
+        p = mp_encode_uint(p, tarantool.IPROTO_KEY)
+        buffer._length += (p - begin)
+        p = encode_key_sequence(buffer, p, self.key, self.index.metadata, False)
diff --git a/asynctnt/iproto/requests/eval.pxd b/asynctnt/iproto/requests/eval.pxd
new file mode 100644
index 0000000..512f765
--- /dev/null
+++ b/asynctnt/iproto/requests/eval.pxd
@@ -0,0 +1,4 @@
+cdef class EvalRequest(BaseRequest):
+    cdef:
+        str expression
+        object args
diff --git a/asynctnt/iproto/requests/eval.pyx b/asynctnt/iproto/requests/eval.pyx
new file mode 100644
index 0000000..49809f7
--- /dev/null
+++ b/asynctnt/iproto/requests/eval.pyx
@@ -0,0 +1,43 @@
+cimport cython
+
+@cython.final
+cdef class EvalRequest(BaseRequest):
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        cdef:
+            char *begin
+            char *p
+            uint32_t body_map_sz
+            uint32_t max_body_len
+
+            bytes expression_temp
+            char *expression_str
+            ssize_t expression_len
+
+        expression_str = NULL
+        expression_len = 0
+
+        expression_temp = encode_unicode_string(self.expression, buffer._encoding)
+        cpython.bytes.PyBytes_AsStringAndSize(expression_temp,
+                                              &expression_str,
+                                              &expression_len)
+        body_map_sz = 2
+        # Size description:
+        # mp_sizeof_map()
+        # + mp_sizeof_uint(TP_EXPRESSION)
+        # + mp_sizeof_str(expression)
+        # + mp_sizeof_uint(TP_TUPLE)
+        max_body_len = 1 \
+                       + 1 \
+                       + mp_sizeof_str(<uint32_t> expression_len) \
+                       + 1
+
+        buffer.ensure_allocated(max_body_len)
+
+        p = begin = &buffer._buf[buffer._length]
+        p = mp_encode_map(p, body_map_sz)
+        p = mp_encode_uint(p, tarantool.IPROTO_EXPR)
+        p = mp_encode_str(p, expression_str, <uint32_t> expression_len)
+
+        p = mp_encode_uint(p, tarantool.IPROTO_TUPLE)
+        buffer._length += (p - begin)
+        p = encode_key_sequence(buffer, p, self.args, None, False)
diff --git a/asynctnt/iproto/requests/execute.pxd b/asynctnt/iproto/requests/execute.pxd
new file mode 100644
index 0000000..985731a
--- /dev/null
+++ b/asynctnt/iproto/requests/execute.pxd
@@ -0,0 +1,5 @@
+cdef class ExecuteRequest(BaseRequest):
+    cdef:
+        str query
+        uint64_t statement_id
+        object args
diff --git a/asynctnt/iproto/requests/execute.pyx b/asynctnt/iproto/requests/execute.pyx
new file mode 100644
index 0000000..c587213
--- /dev/null
+++ b/asynctnt/iproto/requests/execute.pyx
@@ -0,0 +1,62 @@
+cimport cython
+
+@cython.final
+cdef class ExecuteRequest(BaseRequest):
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        cdef:
+            char *begin
+            char *p
+            uint32_t body_map_sz
+            uint32_t max_body_len
+
+            bytes query_temp
+            char *query_str
+            ssize_t query_len
+            uint32_t kind
+
+        body_map_sz = 2
+        max_body_len = 0
+
+        query_str = NULL
+        query_len = 0
+
+        if self.query is not None:
+            query_temp = encode_unicode_string(self.query, buffer._encoding)
+            cpython.bytes.PyBytes_AsStringAndSize(query_temp,
+                                                  &query_str,
+                                                  &query_len)
+            # Size description:
+            # mp_sizeof_map()
+            # + mp_sizeof_uint(TP_SQL_TEXT)
+            # + mp_sizeof_str(query)
+            # + mp_sizeof_uint(TP_SQL_BIND)
+            max_body_len = 1 \
+                           + 1 \
+                           + mp_sizeof_str(<uint32_t> query_len) \
+                           + 1
+            kind = tarantool.IPROTO_SQL_TEXT
+        else:
+            # Size description:
+            # mp_sizeof_map()
+            # + mp_sizeof_uint(IPROTO_STMT_ID)
+            # + mp_sizeof_int(self.statement_id)
+            # + mp_sizeof_uint(TP_SQL_BIND)
+            max_body_len = 1 \
+                           + 1 \
+                           + 9 \
+                           + 1
+            kind = tarantool.IPROTO_STMT_ID
+
+        buffer.ensure_allocated(max_body_len)
+
+        p = begin = &buffer._buf[buffer._length]
+        p = mp_encode_map(p, body_map_sz)
+        p = mp_encode_uint(p, kind)
+        if query_str != NULL:
+            p = mp_encode_str(p, query_str, <uint32_t> query_len)
+        else:
+            p = mp_encode_uint(p, self.statement_id)
+
+        p = mp_encode_uint(p, tarantool.IPROTO_SQL_BIND)
+        buffer._length += (p - begin)
+        p = encode_key_sequence(buffer, p, self.args, None, False)
diff --git a/asynctnt/iproto/requests/id.pxd b/asynctnt/iproto/requests/id.pxd
new file mode 100644
index 0000000..ca86944
--- /dev/null
+++ b/asynctnt/iproto/requests/id.pxd
@@ -0,0 +1,2 @@
+cdef class IDRequest(BaseRequest):
+    pass
diff --git a/asynctnt/iproto/requests/id.pyx b/asynctnt/iproto/requests/id.pyx
new file mode 100644
index 0000000..4edda6b
--- /dev/null
+++ b/asynctnt/iproto/requests/id.pyx
@@ -0,0 +1,41 @@
+cimport cython
+
+DEF IPROTO_VERSION = 3
+
+@cython.final
+cdef class IDRequest(BaseRequest):
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        cdef:
+            char *p
+            char *begin
+            uint32_t body_map_sz
+            uint32_t max_body_len
+
+        body_map_sz = 2
+        # Size description:
+        # mp_sizeof_map(body_map_sz)
+        # + mp_sizeof_uint(tarantool.IPROTO_VERSION)
+        # + mp_sizeof_uint(IPROTO_VERSION)
+        # + mp_sizeof_uint(tarantool.IPROTO_FEATURES)
+        # + mp_sizeof_array(0)
+        # + max arr size
+        max_body_len = 1 \
+                       + 1 \
+                       + 1 \
+                       + 1 \
+                       + 1 \
+                       + 4  # Note: maximum 4 elements in array
+
+        buffer.ensure_allocated(max_body_len)
+
+        p = begin = &buffer._buf[buffer._length]
+        p = mp_encode_map(p, body_map_sz)
+        p = mp_encode_uint(p, tarantool.IPROTO_VERSION)
+        p = mp_encode_uint(p, IPROTO_VERSION)
+        p = mp_encode_uint(p, tarantool.IPROTO_FEATURES)
+        p = mp_encode_array(p, 3)
+        p = mp_encode_uint(p, tarantool.IPROTO_FEATURE_STREAMS)
+        p = mp_encode_uint(p, tarantool.IPROTO_FEATURE_TRANSACTIONS)
+        p = mp_encode_uint(p, tarantool.IPROTO_FEATURE_ERROR_EXTENSION)
+
+        buffer._length += (p - begin)
diff --git a/asynctnt/iproto/requests/insert.pxd b/asynctnt/iproto/requests/insert.pxd
new file mode 100644
index 0000000..50af07b
--- /dev/null
+++ b/asynctnt/iproto/requests/insert.pxd
@@ -0,0 +1,3 @@
+cdef class InsertRequest(BaseRequest):
+    cdef:
+        object t
diff --git a/asynctnt/iproto/requests/insert.pyx b/asynctnt/iproto/requests/insert.pyx
new file mode 100644
index 0000000..a5b4fd1
--- /dev/null
+++ b/asynctnt/iproto/requests/insert.pyx
@@ -0,0 +1,36 @@
+cimport cython
+
+@cython.final
+cdef class InsertRequest(BaseRequest):
+
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        cdef:
+            char *begin
+            char *p
+            uint32_t body_map_sz
+            uint32_t max_body_len
+            uint32_t space_id
+
+        space_id = self.space.sid
+
+        body_map_sz = 2
+        # Size description:
+        # mp_sizeof_map(body_map_sz)
+        # + mp_sizeof_uint(TP_SPACE)
+        # + mp_sizeof_uint(space)
+        # + mp_sizeof_uint(TP_TUPLE)
+        max_body_len = 1 \
+                       + 1 \
+                       + 9 \
+                       + 1
+
+        buffer.ensure_allocated(max_body_len)
+
+        p = begin = &buffer._buf[buffer._length]
+        p = mp_encode_map(p, body_map_sz)
+        p = mp_encode_uint(p, tarantool.IPROTO_SPACE_ID)
+        p = mp_encode_uint(p, space_id)
+
+        p = mp_encode_uint(p, tarantool.IPROTO_TUPLE)
+        buffer._length += (p - begin)
+        p = encode_key_sequence(buffer, p, self.t, self.space.metadata, True)
diff --git a/asynctnt/iproto/requests/ping.pxd b/asynctnt/iproto/requests/ping.pxd
new file mode 100644
index 0000000..e48f949
--- /dev/null
+++ b/asynctnt/iproto/requests/ping.pxd
@@ -0,0 +1,3 @@
+cdef class PingRequest(BaseRequest):
+    pass
+
diff --git a/asynctnt/iproto/requests/ping.pyx b/asynctnt/iproto/requests/ping.pyx
new file mode 100644
index 0000000..673a439
--- /dev/null
+++ b/asynctnt/iproto/requests/ping.pyx
@@ -0,0 +1,5 @@
+cimport cython
+
+@cython.final
+cdef class PingRequest(BaseRequest):
+    pass
diff --git a/asynctnt/iproto/requests/prepare.pxd b/asynctnt/iproto/requests/prepare.pxd
new file mode 100644
index 0000000..dcadb2e
--- /dev/null
+++ b/asynctnt/iproto/requests/prepare.pxd
@@ -0,0 +1,4 @@
+cdef class PrepareRequest(BaseRequest):
+    cdef:
+        str query
+        uint64_t statement_id
diff --git a/asynctnt/iproto/requests/prepare.pyx b/asynctnt/iproto/requests/prepare.pyx
new file mode 100644
index 0000000..2d948f8
--- /dev/null
+++ b/asynctnt/iproto/requests/prepare.pyx
@@ -0,0 +1,60 @@
+cimport cython
+
+@cython.final
+cdef class PrepareRequest(BaseRequest):
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        cdef:
+            char *begin
+            char *p
+            uint32_t body_map_sz
+            uint32_t max_body_len
+
+            bytes query_temp
+            char *query_str
+            ssize_t query_len
+            uint32_t kind
+
+        body_map_sz = 1
+        max_body_len = 0
+
+        query_str = NULL
+        query_len = 0
+
+        if self.query is not None:
+            query_temp = encode_unicode_string(self.query, buffer._encoding)
+            cpython.bytes.PyBytes_AsStringAndSize(query_temp,
+                                                  &query_str,
+                                                  &query_len)
+            # Size description:
+            # mp_sizeof_map()
+            # + mp_sizeof_uint(TP_SQL_TEXT)
+            # + mp_sizeof_str(query)
+            # + mp_sizeof_uint(TP_SQL_BIND)
+            max_body_len = 1 \
+                           + 1 \
+                           + mp_sizeof_str(<uint32_t> query_len) \
+                           + 1
+            kind = tarantool.IPROTO_SQL_TEXT
+        else:
+            # Size description:
+            # mp_sizeof_map()
+            # + mp_sizeof_uint(IPROTO_STMT_ID)
+            # + mp_sizeof_int(self.statement_id)
+            # + mp_sizeof_uint(TP_SQL_BIND)
+            max_body_len = 1 \
+                           + 1 \
+                           + 9 \
+                           + 1
+            kind = tarantool.IPROTO_STMT_ID
+
+        buffer.ensure_allocated(max_body_len)
+
+        p = begin = &buffer._buf[buffer._length]
+        p = mp_encode_map(p, body_map_sz)
+        p = mp_encode_uint(p, kind)
+        if query_str != NULL:
+            p = mp_encode_str(p, query_str, <uint32_t> query_len)
+        else:
+            p = mp_encode_uint(p, self.statement_id)
+
+        buffer._length += (p - begin)
diff --git a/asynctnt/iproto/requests/select.pxd b/asynctnt/iproto/requests/select.pxd
new file mode 100644
index 0000000..7348518
--- /dev/null
+++ b/asynctnt/iproto/requests/select.pxd
@@ -0,0 +1,7 @@
+cdef class SelectRequest(BaseRequest):
+    cdef:
+        SchemaIndex index
+        object key
+        uint64_t offset
+        uint64_t limit
+        uint32_t iterator
diff --git a/asynctnt/iproto/requests/select.pyx b/asynctnt/iproto/requests/select.pyx
new file mode 100644
index 0000000..c18c061
--- /dev/null
+++ b/asynctnt/iproto/requests/select.pyx
@@ -0,0 +1,65 @@
+cimport cython
+
+@cython.final
+cdef class SelectRequest(BaseRequest):
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        cdef:
+            char *begin
+            char *p
+            uint32_t body_map_sz
+            uint32_t max_body_len
+            uint32_t space_id, index_id
+
+        space_id = self.space.sid
+        index_id = self.index.iid
+
+        body_map_sz = 3 \
+                      + <uint32_t> (index_id > 0) \
+                      + <uint32_t> (self.offset > 0) \
+                      + <uint32_t> (self.iterator > 0)
+        # Size description:
+        # mp_sizeof_map(body_map_sz)
+        # + mp_sizeof_uint(TP_SPACE)
+        # + mp_sizeof_uint(space)
+        # + mp_sizeof_uint(TP_LIMIT)
+        # + mp_sizeof_uint(limit)
+        max_body_len = 1 \
+                       + 1 \
+                       + 9 \
+                       + 1 \
+                       + 9
+
+        if index_id > 0:
+            # mp_sizeof_uint(TP_INDEX) + mp_sizeof_uint(index_id)
+            max_body_len += 1 + 9
+        if self.offset > 0:
+            # mp_sizeof_uint(TP_OFFSET) + mp_sizeof_uint(offset)
+            max_body_len += 1 + 9
+        if self.iterator > 0:
+            # mp_sizeof_uint(TP_ITERATOR) + mp_sizeof_uint(iterator)
+            max_body_len += 1 + 1
+
+        max_body_len += 1  # mp_sizeof_uint(TP_KEY);
+
+        buffer.ensure_allocated(max_body_len)
+
+        p = begin = &buffer._buf[buffer._length]
+        p = mp_encode_map(p, body_map_sz)
+        p = mp_encode_uint(p, tarantool.IPROTO_SPACE_ID)
+        p = mp_encode_uint(p, space_id)
+        p = mp_encode_uint(p, tarantool.IPROTO_LIMIT)
+        p = mp_encode_uint(p, self.limit)
+
+        if index_id > 0:
+            p = mp_encode_uint(p, tarantool.IPROTO_INDEX_ID)
+            p = mp_encode_uint(p, index_id)
+        if self.offset > 0:
+            p = mp_encode_uint(p, tarantool.IPROTO_OFFSET)
+            p = mp_encode_uint(p, self.offset)
+        if self.iterator > 0:
+            p = mp_encode_uint(p, tarantool.IPROTO_ITERATOR)
+            p = mp_encode_uint(p, self.iterator)
+
+        p = mp_encode_uint(p, tarantool.IPROTO_KEY)
+        buffer._length += (p - begin)
+        p = encode_key_sequence(buffer, p, self.key, self.index.metadata, False)
diff --git a/asynctnt/iproto/requests/streams.pxd b/asynctnt/iproto/requests/streams.pxd
new file mode 100644
index 0000000..451972f
--- /dev/null
+++ b/asynctnt/iproto/requests/streams.pxd
@@ -0,0 +1,10 @@
+cdef class BeginRequest(BaseRequest):
+    cdef:
+        uint32_t isolation
+        double tx_timeout
+
+cdef class CommitRequest(BaseRequest):
+    pass
+
+cdef class RollbackRequest(BaseRequest):
+    pass
diff --git a/asynctnt/iproto/requests/streams.pyx b/asynctnt/iproto/requests/streams.pyx
new file mode 100644
index 0000000..616f8b9
--- /dev/null
+++ b/asynctnt/iproto/requests/streams.pyx
@@ -0,0 +1,37 @@
+cimport cython
+
+@cython.final
+cdef class BeginRequest(BaseRequest):
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        cdef:
+            char *p
+            char *begin
+            uint32_t body_map_sz
+            uint32_t max_body_len
+
+        body_map_sz = 2
+        # Size description:
+        max_body_len = mp_sizeof_map(body_map_sz) \
+                       + mp_sizeof_uint(tarantool.IPROTO_TXN_ISOLATION) \
+                       + mp_sizeof_uint(self.isolation) \
+                       + mp_sizeof_uint(tarantool.IPROTO_TIMEOUT) \
+                       + mp_sizeof_double(self.tx_timeout)
+
+        buffer.ensure_allocated(max_body_len)
+
+        p = begin = &buffer._buf[buffer._length]
+        p = mp_encode_map(p, body_map_sz)
+        p = mp_encode_uint(p, tarantool.IPROTO_TXN_ISOLATION)
+        p = mp_encode_uint(p, self.isolation)
+        p = mp_encode_uint(p, tarantool.IPROTO_TIMEOUT)
+        p = mp_encode_double(p, self.tx_timeout)
+
+        buffer._length += (p - begin)
+
+@cython.final
+cdef class CommitRequest(BaseRequest):
+    pass
+
+@cython.final
+cdef class RollbackRequest(BaseRequest):
+    pass
diff --git a/asynctnt/iproto/requests/update.pxd b/asynctnt/iproto/requests/update.pxd
new file mode 100644
index 0000000..b3c31ce
--- /dev/null
+++ b/asynctnt/iproto/requests/update.pxd
@@ -0,0 +1,14 @@
+cdef class UpdateRequest(BaseRequest):
+    cdef:
+        SchemaIndex index
+        object key
+        list operations
+
+
+cdef char *encode_update_ops(WriteBuffer buffer,
+                             char *p, list operations,
+                             SchemaSpace space) except NULL
+cdef int encode_request_update(WriteBuffer buffer,
+                               SchemaSpace space, SchemaIndex index,
+                               key_tuple, list operations,
+                               bint is_upsert) except -1
diff --git a/asynctnt/iproto/requests/update.pyx b/asynctnt/iproto/requests/update.pyx
new file mode 100644
index 0000000..74ceb9a
--- /dev/null
+++ b/asynctnt/iproto/requests/update.pyx
@@ -0,0 +1,217 @@
+cimport cython
+
+@cython.final
+cdef class UpdateRequest(BaseRequest):
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        return encode_request_update(buffer, self.space, self.index,
+                                     self.key, self.operations, <bint> False)
+
+cdef char *encode_update_ops(WriteBuffer buffer,
+                             char *p, list operations,
+                             SchemaSpace space) except NULL:
+    cdef:
+        char *begin
+        uint32_t ops_len, op_len
+        bytes str_temp
+        char *str_c
+        ssize_t str_len
+
+        char *op_str_c
+        ssize_t op_str_len
+        char op
+
+        uint32_t extra_length
+
+        uint64_t field_no
+        object field_no_obj
+
+        uint32_t splice_position, splice_offset
+
+    begin = NULL
+
+    if operations is not None:
+        ops_len = <uint32_t> cpython.list.PyList_GET_SIZE(operations)
+    else:
+        ops_len = 0
+
+    p = buffer.mp_encode_array(p, ops_len)
+    if ops_len == 0:
+        return p
+
+    for operation in operations:
+        if not isinstance(operation, (list, tuple)):
+            raise TypeError(
+                'Single operation must be a tuple or list')
+
+        op_len = <uint32_t> cpython.list.PyList_GET_SIZE(operation)
+        if op_len < 3:
+            raise IndexError(
+                'Operation length must be at least 3')
+
+        op_type_str = operation[0]
+        if isinstance(op_type_str, str):
+            str_temp = encode_unicode_string(op_type_str, buffer._encoding)
+        elif isinstance(op_type_str, bytes):
+            str_temp = <bytes> op_type_str
+        else:
+            raise TypeError(
+                'Operation type must of a str or bytes type')
+
+        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)
+            else:
+                raise TypeError(
+                    'Operation field_no must be int as there is '
+                    'no format declaration in space {}'.format(space.sid))
+        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]
+
+        if op == tarantool.IPROTO_OP_ADD \
+                or op == tarantool.IPROTO_OP_SUB \
+                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_ASSIGN:
+            op_argument = operation[2]
+
+            # 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_SPLICE:
+            if op_len < 5:
+                raise IndexError(
+                    'Splice operation must have length of 5, '
+                    'but got: {}'.format(op_len)
+                )
+
+            splice_position_obj = operation[2]
+            splice_offset_obj = operation[3]
+            op_argument = operation[4]
+            if not isinstance(splice_position_obj, int):
+                raise TypeError('Splice position must be int')
+            if not isinstance(splice_offset_obj, int):
+                raise TypeError('Splice offset must be int')
+
+            splice_position = <uint32_t> splice_position_obj
+            splice_offset = <uint32_t> splice_offset_obj
+
+            # mp_sizeof_array(5) + mp_sizeof_str(1) + ...
+            extra_length = 1 + 2 \
+                           + mp_sizeof_uint(field_no) \
+                           + mp_sizeof_uint(splice_position) \
+                           + mp_sizeof_uint(splice_offset)
+            p = begin = buffer._ensure_allocated(p, extra_length)
+
+            p = mp_encode_array(p, 5)
+            p = mp_encode_str(p, op_str_c, 1)
+            p = mp_encode_uint(p, field_no)
+            p = mp_encode_uint(p, splice_position)
+            p = mp_encode_uint(p, splice_offset)
+            buffer._length += (p - begin)
+            p = buffer.mp_encode_obj(p, op_argument)
+        else:
+            raise TypeError(
+                'Unknown update operation type `{}`'.format(op_type_str))
+    return p
+
+cdef int encode_request_update(WriteBuffer buffer,
+                               SchemaSpace space, SchemaIndex index,
+                               key_tuple, list operations,
+                               bint is_upsert) except -1:
+    cdef:
+        char *begin
+        char *p
+        uint32_t body_map_sz
+        uint32_t max_body_len
+        uint32_t space_id, index_id
+        uint32_t key_of_tuple, key_of_operations
+        Metadata metadata
+        bint default_fields_none
+
+    space_id = space.sid
+    index_id = index.iid
+
+    if not is_upsert:
+        key_of_tuple = tarantool.IPROTO_KEY
+        key_of_operations = tarantool.IPROTO_TUPLE
+        metadata = index.metadata
+        default_fields_none = False
+    else:
+        key_of_tuple = tarantool.IPROTO_TUPLE
+        key_of_operations = tarantool.IPROTO_OPS
+        metadata = space.metadata
+        default_fields_none = True
+
+    body_map_sz = 3 + <uint32_t> (index_id > 0)
+    # Size description:
+    # mp_sizeof_map(body_map_sz)
+    # + mp_sizeof_uint(TP_SPACE)
+    # + mp_sizeof_uint(space)
+    max_body_len = 1 \
+                   + 1 \
+                   + 9
+
+    if index_id > 0:
+        # + mp_sizeof_uint(TP_INDEX)
+        # + mp_sizeof_uint(index)
+        max_body_len += 1 + 9
+
+    max_body_len += 1  # + mp_sizeof_uint(TP_KEY)
+    max_body_len += 1  # + mp_sizeof_uint(TP_TUPLE)
+
+    buffer.ensure_allocated(max_body_len)
+
+    p = begin = &buffer._buf[buffer._length]
+    p = mp_encode_map(p, body_map_sz)
+    p = mp_encode_uint(p, tarantool.IPROTO_SPACE_ID)
+    p = mp_encode_uint(p, space_id)
+
+    if index_id > 0:
+        p = mp_encode_uint(p, tarantool.IPROTO_INDEX_ID)
+        p = mp_encode_uint(p, index_id)
+    buffer._length += (p - begin)
+
+    p = buffer.mp_encode_uint(p, key_of_tuple)
+    p = encode_key_sequence(buffer, p, key_tuple, metadata, default_fields_none)
+
+    p = buffer.mp_encode_uint(p, key_of_operations)
+    p = encode_update_ops(buffer, p, operations, space)
diff --git a/asynctnt/iproto/requests/upsert.pxd b/asynctnt/iproto/requests/upsert.pxd
new file mode 100644
index 0000000..92bd5e6
--- /dev/null
+++ b/asynctnt/iproto/requests/upsert.pxd
@@ -0,0 +1,4 @@
+cdef class UpsertRequest(BaseRequest):
+    cdef:
+        object t
+        list operations
diff --git a/asynctnt/iproto/requests/upsert.pyx b/asynctnt/iproto/requests/upsert.pyx
new file mode 100644
index 0000000..2058ed3
--- /dev/null
+++ b/asynctnt/iproto/requests/upsert.pyx
@@ -0,0 +1,7 @@
+cimport cython
+
+@cython.final
+cdef class UpsertRequest(BaseRequest):
+    cdef int encode_body(self, WriteBuffer buffer) except -1:
+        return encode_request_update(buffer, self.space, self.space.get_index(0),
+                                     self.t, self.operations, <bint> True)
diff --git a/asynctnt/iproto/response.pxd b/asynctnt/iproto/response.pxd
index e0a3ede..fbc79ad 100644
--- a/asynctnt/iproto/response.pxd
+++ b/asynctnt/iproto/response.pxd
@@ -1,3 +1,5 @@
+cimport cython
+
 from libc.stdint cimport uint64_t, uint32_t, int64_t, int32_t
 
 cdef struct Header:
@@ -6,20 +8,38 @@ cdef struct Header:
     uint64_t sync
     int64_t schema_id
 
+cdef class IProtoErrorStackFrame:
+    cdef:
+        readonly str error_type
+        readonly str file
+        readonly int line
+        readonly str message
+        readonly int err_no
+        readonly int code
+        readonly dict fields
+
+cdef class IProtoError:
+    cdef:
+        readonly list trace
 
 cdef class Response:
     cdef:
-        int32_t _code
-        int32_t _return_code
-        uint64_t _sync
-        int64_t _schema_id
-        str _errmsg
+        int32_t code_
+        int32_t return_code_
+        uint64_t sync_
+        int64_t schema_id_
+        readonly str errmsg
+        readonly IProtoError error
         int _rowcount
-        list _body
-        bytes _encoding
-        TntFields _fields
-        list _autoincrement_ids
+        readonly list body
+        readonly bytes encoding
+        readonly Metadata metadata
+        readonly Metadata params
+        readonly int params_count
+        readonly list autoincrement_ids
+        uint64_t stmt_id_
         bint _push_subscribe
+        BaseRequest request_
         object _exception
 
         readonly object _q
@@ -31,17 +51,18 @@ cdef class Response:
 
     cdef inline bint is_error(self)
     cdef inline uint32_t _len(self)
+    cdef inline void init_push(self)
     cdef inline void add_push(self, push)
     cdef inline object pop_push(self)
-    cdef inline object push_len(self)
+    cdef inline int push_len(self)
     cdef inline void set_data(self, list data)
     cdef inline void set_exception(self, exc)
     cdef inline object get_exception(self)
     cdef inline void notify(self)
 
-
 cdef ssize_t response_parse_header(const char *buf, uint32_t buf_len,
                                    Header *hdr) except -1
 cdef ssize_t response_parse_body(const char *buf, uint32_t buf_len,
-                                 Response resp, Request req,
+                                 Response resp, BaseRequest req,
                                  bint is_chunk) except -1
+cdef IProtoError parse_iproto_error(const char ** b, bytes encoding)
diff --git a/asynctnt/iproto/response.pyx b/asynctnt/iproto/response.pyx
index c22b97f..37c97bf 100644
--- a/asynctnt/iproto/response.pyx
+++ b/asynctnt/iproto/response.pyx
@@ -5,12 +5,25 @@ from typing import Optional
 cimport cpython
 cimport cpython.list
 cimport cpython.dict
+cimport cython
 
 from libc.stdint cimport uint32_t
 from libc cimport stdio
 
 from asynctnt.log import logger
 
+@cython.final
+cdef class IProtoErrorStackFrame:
+    def __repr__(self):
+        return "<Frame type={} code={} message={}>".format(
+            self.error_type,
+            self.code,
+            self.message,
+        )
+
+@cython.final
+cdef class IProtoError:
+    pass
 
 @cython.final
 @cython.freelist(REQUEST_FREELIST)
@@ -19,32 +32,44 @@ cdef class Response:
         Response object for all the requests to Tarantool
     """
 
-    def __cinit__(self, bytes encoding, bint push_subscribe):
-        self._sync = 0
-        self._code = -1
-        self._return_code = -1
-        self._schema_id = -1
-        self._errmsg = None
+    def __cinit__(self):
+        self.request_ = None
+        self.sync_ = 0
+        self.code_ = -1
+        self.return_code_ = -1
+        self.schema_id_ = -1
+        self.errmsg = None
+        self.error = None
         self._rowcount = 0
-        self._body = None
-        self._encoding = encoding
-        self._fields = None
-        self._autoincrement_ids = None
-        self._push_subscribe = push_subscribe
-        if push_subscribe:
-            self._q = collections.deque()
-            self._push_event = asyncio.Event()
-
-            self._q_append = self._q.append
-            self._q_popleft = self._q.popleft
-            self._push_event_set = self._push_event.set
-            self._push_event_clear = self._push_event.clear
-        else:
-            self._q = None
-            self._push_event = None
+        self.body = None
+        self.encoding = None
+        self.metadata = None
+        self.params = None
+        self.params_count = 0
+        self.autoincrement_ids = None
+        self.stmt_id_ = 0
+
+        self._push_subscribe = False
+        self._q = None
+        self._push_event = None
+        self._q_append = None
+        self._q_popleft = None
+        self._push_event_set = None
+        self._push_event_clear = None
 
     cdef inline bint is_error(self):
-        return self._code >= 0x8000
+        return self.code_ >= 0x8000
+
+    # noinspection PyAttributeOutsideInit
+    cdef inline void init_push(self):
+        self._push_subscribe = True
+        self._q = collections.deque()
+        self._push_event = asyncio.Event()
+
+        self._q_append = self._q.append
+        self._q_popleft = self._q.popleft
+        self._push_event_set = self._push_event.set
+        self._push_event_clear = self._push_event.clear
 
     cdef inline void add_push(self, push):
         if not self._push_subscribe:
@@ -53,7 +78,7 @@ cdef class Response:
         self._q_append(push)
         self.notify()
 
-    cdef inline object push_len(self):
+    cdef inline int push_len(self):
         return len(self._q)
 
     cdef inline object pop_push(self):
@@ -66,7 +91,7 @@ cdef class Response:
         return push
 
     cdef inline void set_data(self, list data):
-        self._body = data
+        self.body = data
         self.notify()
 
     cdef inline void set_exception(self, exc):
@@ -81,7 +106,7 @@ cdef class Response:
             self._push_event_set()  # Notify that there is no more data
 
     def __repr__(self):  # pragma: nocover
-        data = self._body
+        data = self.body
         if data is not None:
             if len(data) > 10:
                 parts = map(lambda x: ', '.join(map(repr, x)), [
@@ -100,85 +125,56 @@ cdef class Response:
         """
             Response's sync (incremental id) for the corresponding request
         """
-        return self._sync
+        return <int> self.sync_
 
     @property
     def code(self) -> int:
         """
             Response code (0 - success)
         """
-        return self._code
+        return <int> self.code_
 
     @property
     def return_code(self) -> int:
         """
             Response return code (It's essentially a code & 0x7FFF)
         """
-        return self._return_code
+        return <int> self.return_code_
 
     @property
     def schema_id(self) -> int:
-        """
-            Current scema id in Tarantool
-        """
-        return self._schema_id
-
-    @property
-    def errmsg(self) -> Optional[str]:
-        """
-            If self.code != 0 then errmsg contains an error message
-        """
-        return self._errmsg
-
-    @property
-    def body(self) -> Optional[list]:
-        """
-            Response body
-        """
-        return self._body
-
-    @property
-    def encoding(self) -> str:
-        """
-            Response encoding
-        """
-        return self._encoding
+        return <int> self.schema_id_
 
     @property
     def rowcount(self) -> int:
-        if self._body is not None:
+        if self.body is not None:
             self_len = self._len()
             if self_len > 0:
                 return self_len
         return self._rowcount
 
     @property
-    def autoincrement_ids(self) -> Optional[list]:
-        """
-            Response autoincrement ids
-        """
-        return self._autoincrement_ids
-
+    def stmt_id(self) -> Optional[int]:
+        return self.stmt_id_
 
     def done(self):
-        return self._code >= 0
+        return self.code_ >= 0
 
     cdef inline uint32_t _len(self):
-        return <uint32_t>cpython.list.PyList_GET_SIZE(self._body)
+        return <uint32_t> cpython.list.PyList_GET_SIZE(self.body)
 
     def __len__(self) -> int:
-        if self._body is not None:
-            return <int>self._len()
+        if self.body is not None:
+            return <int> self._len()
         return 0
 
     def __getitem__(self, i):
-        return self._body[i]
+        return self.body[i]
 
     def __iter__(self):
-        return iter(self._body)
-
+        return iter(self.body)
 
-cdef object _decode_obj(const char **p, bytes encoding):
+cdef object _decode_obj(const char ** p, bytes encoding):
     cdef:
         uint32_t i
         mp_type obj_type
@@ -196,6 +192,9 @@ cdef object _decode_obj(const char **p, bytes encoding):
         uint32_t map_key_len
         object map_key
 
+        int8_t ext_type
+        IProtoDateTime dt
+
     obj_type = mp_typeof(p[0][0])
     if obj_type == MP_UINT:
         return mp_decode_uint(p)
@@ -208,12 +207,12 @@ cdef object _decode_obj(const char **p, bytes encoding):
         try:
             return decode_string(s[:s_len], encoding)
         except UnicodeDecodeError:
-            return <bytes>s[:s_len]
+            return <bytes> s[:s_len]
     elif obj_type == MP_BIN:
         s = NULL
         s_len = 0
         s = mp_decode_bin(p, &s_len)
-        return <bytes>s[:s_len]
+        return <bytes> s[:s_len]
     elif obj_type == MP_BOOL:
         return mp_decode_bool(p)
     elif obj_type == MP_FLOAT:
@@ -236,17 +235,17 @@ cdef object _decode_obj(const char **p, bytes encoding):
             if map_key_type == MP_STR:
                 map_key_len = 0
                 map_key_str = mp_decode_str(p, &map_key_len)
-                map_key = <object>(
+                map_key = <object> (
                     decode_string(map_key_str[:map_key_len], encoding)
                 )
             elif map_key_type == MP_UINT:
-                map_key = <object>mp_decode_uint(p)
+                map_key = <object> mp_decode_uint(p)
             elif map_key_type == MP_INT:
-                map_key = <object>mp_decode_int(p)
+                map_key = <object> mp_decode_int(p)
             elif map_key_type == MP_FLOAT:
-                map_key = <object>mp_decode_float(p)
+                map_key = <object> mp_decode_float(p)
             elif map_key_type == MP_DOUBLE:
-                map_key = <object>mp_decode_double(p)
+                map_key = <object> mp_decode_double(p)
             else:  # pragma: nocover
                 mp_next(p)  # skip current key
                 mp_next(p)  # skip value
@@ -260,21 +259,37 @@ cdef object _decode_obj(const char **p, bytes encoding):
     elif obj_type == MP_NIL:
         mp_next(p)
         return None
+    elif obj_type == MP_EXT:
+        ext_type = 0
+        s_len = mp_decode_extl(p, &ext_type)
+        if ext_type == tarantool.MP_DECIMAL:
+            return decimal_decode(p, s_len)
+        elif ext_type == tarantool.MP_UUID:
+            return uuid_decode(p, s_len)
+        elif ext_type == tarantool.MP_ERROR:
+            return parse_iproto_error(p, encoding)
+        elif ext_type == tarantool.MP_DATETIME:
+            datetime_zero(&dt)
+            datetime_decode(p, s_len, &dt)
+            return datetime_to_py(&dt)
+        else:  # pragma: nocover
+            logger.warning('Unexpected ext type: %d', ext_type)
+            p += s_len  # skip unknown ext
+            return None
     else:  # pragma: nocover
         mp_next(p)
         logger.warning('Unexpected obj type: %s', obj_type)
         return None
 
-
-cdef list _response_parse_body_data(const char **b,
-                                    Response resp, Request req):
+cdef list _response_parse_body_data(const char ** b,
+                                    Response resp, BaseRequest req):
     cdef:
         uint32_t size
         uint32_t tuple_size
         list tuples
         uint32_t i
 
-        TntFields fields
+        Metadata metadata
 
     size = mp_decode_array(b)
     tuples = []
@@ -282,9 +297,9 @@ cdef list _response_parse_body_data(const char **b,
     if req.parse_as_tuples:
         # decode as TarantoolTuples
 
-        fields = resp._fields
-        if fields is None:
-            fields = req.fields()
+        metadata = resp.metadata
+        if metadata is None:
+            metadata = req.metadata()
 
         for i in range(size):
             if mp_typeof(b[0][0]) != MP_ARRAY:  # pragma: nocover
@@ -293,9 +308,9 @@ cdef list _response_parse_body_data(const char **b,
                 )
 
             tuple_size = mp_decode_array(b)
-            t = tupleobj.AtntTuple_New(fields, <int>tuple_size)
+            t = tupleobj.AtntTuple_New(metadata, <int> tuple_size)
             for i in range(tuple_size):
-                value = _decode_obj(b, resp._encoding)
+                value = _decode_obj(b, resp.encoding)
                 cpython.Py_INCREF(value)
                 tupleobj.AtntTuple_SET_ITEM(t, i, value)
 
@@ -303,11 +318,10 @@ cdef list _response_parse_body_data(const char **b,
     else:
         # decode as raw objects
         for i in range(size):
-            tuples.append(_decode_obj(b, resp._encoding))
+            tuples.append(_decode_obj(b, resp.encoding))
 
     return tuples
 
-
 cdef ssize_t response_parse_header(const char *buf, uint32_t buf_len,
                                    Header *hdr) except -1:
     cdef:
@@ -315,7 +329,7 @@ cdef ssize_t response_parse_header(const char *buf, uint32_t buf_len,
         uint32_t size
         uint32_t key
 
-    b = <const char*>buf
+    b = <const char *> buf
     # mp_fprint(stdio.stdout, b)
     # stdio.fprintf(stdio.stdout, "\n")
 
@@ -333,7 +347,7 @@ cdef ssize_t response_parse_header(const char *buf, uint32_t buf_len,
             if mp_typeof(b[0]) != MP_UINT:  # pragma: nocover
                 raise TypeError('code type must be a MP_UINT')
 
-            hdr.code = <uint32_t>mp_decode_uint(&b)
+            hdr.code = <uint32_t> mp_decode_uint(&b)
             hdr.return_code = hdr.code & 0x7FFF
         elif key == tarantool.IPROTO_SYNC:
             if mp_typeof(b[0]) != MP_UINT:  # pragma: nocover
@@ -350,11 +364,176 @@ cdef ssize_t response_parse_header(const char *buf, uint32_t buf_len,
                 'Unknown key with code \'%d\' in header. Skipping.', key)
             mp_next(&b)
 
-    return <ssize_t>(b - buf)
+    return <ssize_t> (b - buf)
+
+cdef Metadata response_parse_metadata(const char ** b, bytes encoding):
+    cdef:
+        uint32_t arr_size
+        uint32_t field_map_size
+        uint32_t key
+        uint32_t s_len
+        uint32_t i
+        const char *s
+        Field field
+        Metadata metadata
+
+    metadata = <Metadata> Metadata.__new__(Metadata)
+    arr_size = mp_decode_array(b)
+    for i in range(arr_size):
+        field_map_size = mp_decode_map(b)
+        if field_map_size == 0:  # pragma: nocover
+            raise RuntimeError('Field map must contain at least '
+                               '1 element - field_name')
+
+        field = <Field> Field.__new__(Field)
+        field_id = i
+        for _ in range(field_map_size):
+            key = mp_decode_uint(b)
+            if key == tarantool.IPROTO_FIELD_NAME:
+                s = NULL
+                s_len = 0
+                s = mp_decode_str(b, &s_len)
+                field.name = \
+                    decode_string(s[:s_len], encoding)
+
+            elif key == tarantool.IPROTO_FIELD_TYPE:
+                s = NULL
+                s_len = 0
+                s = mp_decode_str(b, &s_len)
+                field.type = \
+                    decode_string(s[:s_len], encoding)
+
+            elif key == tarantool.IPROTO_FIELD_COLL:
+                s = NULL
+                s_len = 0
+                s = mp_decode_str(b, &s_len)
+                field.collation = \
+                    decode_string(s[:s_len], encoding)
+
+            elif key == tarantool.IPROTO_FIELD_IS_NULLABLE:
+                field.is_nullable = mp_decode_bool(b)
+
+            elif key == tarantool.IPROTO_FIELD_IS_AUTOINCREMENT:
+                field.is_autoincrement = mp_decode_bool(b)
+
+            elif key == tarantool.IPROTO_FIELD_SPAN:
+                if mp_typeof(b[0][0]) == MP_NIL:  # pragma: nocover
+                    mp_next(b)
+                    field.span = None
+
+                elif mp_typeof(b[0][0]) == MP_STR:
+                    s = NULL
+                    s_len = 0
+                    s = mp_decode_str(b, &s_len)
+                    field.span = \
+                        decode_string(s[:s_len], encoding)
+
+                else:  # pragma: nocover
+                    raise TypeError(
+                        "IPROTO_FIELD_SPAN must be either STR or NIL"
+                    )
+            else:  # pragma: nocover
+                logger.debug(
+                    'unknown key in metadata decoding: %d', key)
+                mp_next(b)
+
+        if field.name is None:  # pragma: nocover
+            raise RuntimeError('field.name must not be None')
+
+        metadata.add(<int> field_id, field)
+    return metadata
+
+cdef inline IProtoErrorStackFrame parse_iproto_error_stack_frame(const char ** b, bytes encoding):
+    cdef:
+        uint32_t size
+        uint32_t key
+        const char * s
+        uint32_t s_len
+        IProtoErrorStackFrame frame
+        uint32_t unum
+
+    size = 0
+    key = 0
+
+    frame = <IProtoErrorStackFrame> IProtoErrorStackFrame.__new__(IProtoErrorStackFrame)
+
+    size = mp_decode_map(b)
+    for _ in range(size):
+        key = mp_decode_uint(b)
+
+        if key == tarantool.MP_ERROR_TYPE:
+            s = NULL
+            s_len = 0
+            s = mp_decode_str(b, &s_len)
+            frame.error_type = decode_string(s[:s_len], encoding)
+
+        elif key == tarantool.MP_ERROR_FILE:
+            s = NULL
+            s_len = 0
+            s = mp_decode_str(b, &s_len)
+            frame.file = decode_string(s[:s_len], encoding)
+
+        elif key == tarantool.MP_ERROR_LINE:
+            frame.line = <int> mp_decode_uint(b)
+
+        elif key == tarantool.MP_ERROR_MESSAGE:
+            s = NULL
+            s_len = 0
+            s = mp_decode_str(b, &s_len)
+            frame.message = decode_string(s[:s_len], encoding)
+
+        elif key == tarantool.MP_ERROR_ERRNO:
+            frame.err_no = <int> mp_decode_uint(b)
+
+        elif key == tarantool.MP_ERROR_ERRCODE:
+            frame.code = <int> mp_decode_uint(b)
+
+        elif key == tarantool.MP_ERROR_FIELDS:
+            if mp_typeof(b[0][0]) != MP_MAP:  # pragma: nocover
+                raise TypeError(f'iproto_error stack frame fields must be a '
+                                f'map, but got {mp_typeof(b[0][0])}')
+
+            frame.fields = _decode_obj(b, encoding)
+
+        else:  # pragma: nocover
+            logger.debug(f"unknown iproto_error stack element with key {key}")
+            mp_next(b)
+
+    return frame
+
+cdef inline IProtoError parse_iproto_error(const char ** b, bytes encoding):
+    cdef:
+        uint32_t size
+        uint32_t arr_size
+        uint32_t key
+        uint32_t i
+        IProtoError error
+
+    size = 0
+    arr_size = 0
+    key = 0
+
+    error = <IProtoError> IProtoError.__new__(IProtoError)
+
+    size = mp_decode_map(b)
+    for _ in range(size):
+        key = mp_decode_uint(b)
+
+        if key == tarantool.MP_ERROR_STACK:
+            arr_size = mp_decode_array(b)
+            error.trace = cpython.list.PyList_New(arr_size)
+            for i in range(arr_size):
+                el = parse_iproto_error_stack_frame(b, encoding)
+                cpython.Py_INCREF(el)
+                cpython.list.PyList_SET_ITEM(error.trace, i, el)
+        else:  # pragma: nocover
+            logger.debug(f"unknown iproto_error map field with key {key}")
+            mp_next(b)
 
+    return error
 
 cdef ssize_t response_parse_body(const char *buf, uint32_t buf_len,
-                                 Response resp, Request req,
+                                 Response resp, BaseRequest req,
                                  bint is_chunk) except -1:
     cdef:
         const char *b
@@ -366,9 +545,9 @@ cdef ssize_t response_parse_body(const char *buf, uint32_t buf_len,
         uint32_t i
         const char *s
         list data
-        str field_name, field_type
+        Field field
 
-    b = <const char*>buf
+    b = <const char *> buf
     # mp_fprint(stdio.stdout, b)
     # stdio.fprintf(stdio.stdout, "\n")
 
@@ -383,54 +562,43 @@ cdef ssize_t response_parse_body(const char *buf, uint32_t buf_len,
             raise TypeError('Header key must be a MP_UINT')
 
         key = mp_decode_uint(&b)
-        if key == tarantool.IPROTO_ERROR:
+        if key == tarantool.IPROTO_ERROR_24:
             if mp_typeof(b[0]) != MP_STR:  # pragma: nocover
                 raise TypeError('errstr type must be a MP_STR')
 
             s = NULL
             s_len = 0
             s = mp_decode_str(&b, &s_len)
-            resp._errmsg = decode_string(s[:s_len], resp._encoding)
+            resp.errmsg = decode_string(s[:s_len], resp.encoding)
+
+        elif key == tarantool.IPROTO_ERROR:
+            if mp_typeof(b[0]) != MP_MAP:  # pragma: nocover
+                raise TypeError('IPROTO_ERROR type must be a MP_MAP')
+
+            resp.error = parse_iproto_error(&b, resp.encoding)
+
+        elif key == tarantool.IPROTO_STMT_ID:
+            if mp_typeof(b[0]) != MP_UINT:  # pragma: nocover
+                raise TypeError(f'IPROTO_STMT_ID type must be a MP_UINT, but got {mp_typeof(b[0])}')
+            resp.stmt_id_ = mp_decode_uint(&b)
+
         elif key == tarantool.IPROTO_METADATA:
             if not req.parse_metadata:
                 mp_next(&b)
                 continue
 
-            resp._fields = TntFields.__new__(TntFields)
+            resp.metadata = response_parse_metadata(&b, resp.encoding)
 
-            arr_size = mp_decode_array(&b)
-            for i in range(arr_size):
-                field_map_size = mp_decode_map(&b)
-                if field_map_size == 0:
-                    raise RuntimeError('Field map must contain at least '
-                                       '1 element - field_name')
-
-                field_id = i
-                field_name = None
-                field_type = None
-                for _ in range(field_map_size):
-                    key = mp_decode_uint(&b)
-                    if key == tarantool.IPROTO_FIELD_NAME:
-                        s = NULL
-                        s_len = 0
-                        s = mp_decode_str(&b, &s_len)
-                        field_name = \
-                            decode_string(s[:s_len], resp._encoding)
-                    elif key == tarantool.IPROTO_FIELD_TYPE:
-                        s = NULL
-                        s_len = 0
-                        s = mp_decode_str(&b, &s_len)
-                        field_type = \
-                            decode_string(s[:s_len], resp._encoding)
-                    else:
-                        logger.warning(
-                            'unknown key in metadata decoding: %d', key)
-                        mp_next(&b)
-
-                if field_name is None:
-                    raise RuntimeError('field_name must not be None')
-
-                resp._fields.add(field_id, field_name)
+        elif key == tarantool.IPROTO_BIND_METADATA:
+            if not req.parse_metadata:
+                mp_next(&b)
+                continue
+
+            resp.params = response_parse_metadata(&b, resp.encoding)
+
+        elif key == tarantool.IPROTO_BIND_COUNT:
+
+            resp.params_count = <int> mp_decode_uint(&b)
 
         elif key == tarantool.IPROTO_SQL_INFO:
             field_map_size = mp_decode_map(&b)
@@ -446,12 +614,12 @@ cdef ssize_t response_parse_body(const char *buf, uint32_t buf_len,
                     arr_size = mp_decode_array(&b)
                     ids = cpython.list.PyList_New(arr_size)
                     for i in range(arr_size):
-                        el = <object>mp_decode_uint(&b)
+                        el = <object> mp_decode_uint(&b)
                         cpython.Py_INCREF(el)
                         cpython.list.PyList_SET_ITEM(ids, i, el)
-                    resp._autoincrement_ids = ids
+                    resp.autoincrement_ids = ids
                 else:
-                    logger.warning('unknown key in sql info decoding: %d', key)
+                    logger.debug('unknown key in sql info decoding: %d', key)
                     mp_next(&b)
 
         elif key == tarantool.IPROTO_DATA:
@@ -463,8 +631,14 @@ cdef ssize_t response_parse_body(const char *buf, uint32_t buf_len,
             else:
                 resp.set_data(data)
 
-        else:
-            logger.warning('unknown key in body map: %d', int(key))
+        elif key == tarantool.IPROTO_VERSION:
+            logger.debug("IProto version: %s", _decode_obj(&b, resp.encoding))
+
+        elif key == tarantool.IPROTO_FEATURES:
+            logger.debug("IProto features available: %s", _decode_obj(&b, resp.encoding))
+
+        else:  # pragma: nocover
+            logger.debug('unknown key in body map: %d', int(key))
             mp_next(&b)
 
-    return <ssize_t>(b - buf)
+    return <ssize_t> (b - buf)
diff --git a/asynctnt/iproto/schema.pxd b/asynctnt/iproto/schema.pxd
index e97e05f..6487c75 100644
--- a/asynctnt/iproto/schema.pxd
+++ b/asynctnt/iproto/schema.pxd
@@ -1,26 +1,36 @@
-from libc.stdint cimport uint64_t, int64_t
+from libc.stdint cimport  int64_t
 
 
-cdef public class TntFields [object C_TntFields, type C_TntFields_Type]:
+cdef class Field:
     cdef:
-        list _names  # contains only field names ('f1', 'f2', ...)
-        dict _mapping  # contains field's name => id mappings
+        readonly str name
+        readonly str type
+        readonly str collation
+        readonly object is_nullable
+        readonly object is_autoincrement
+        readonly str span
+
+
+cdef public class Metadata [object C_Metadata, type C_Metadata_Type]:
+    cdef:
+        readonly list fields
+        readonly dict name_id_map
+        list names
 
     cdef inline int len(self)
-    cdef inline void add(self, uint64_t id, str name)
+    cdef inline void add(self, int id, Field field)
     cdef inline str name_by_id(self, int i)
-    cdef inline uint64_t id_by_name(self, str f) except *
+    cdef inline int id_by_name(self, str name) except *
 
 
 cdef class SchemaIndex:
     cdef:
-        int sid
-        int iid
-        str name
-        str index_type
-        object unique
-
-        TntFields fields
+        readonly int sid
+        readonly int iid
+        readonly str name
+        readonly str index_type
+        readonly object unique
+        readonly Metadata metadata
 
 
 cdef class SchemaDummyIndex(SchemaIndex):
@@ -29,15 +39,15 @@ cdef class SchemaDummyIndex(SchemaIndex):
 
 cdef class SchemaSpace:
     cdef:
-        int sid
-        int owner
-        str name
-        str engine
-        int field_count
-        object flags
+        readonly int sid
+        readonly int owner
+        readonly str name
+        readonly str engine
+        readonly int field_count
+        readonly object flags
 
-        TntFields fields
-        dict indexes
+        readonly Metadata metadata
+        readonly dict indexes
 
     cdef void add_index(self, SchemaIndex idx)
     cdef SchemaIndex get_index(self, index, create_dummy=*)
@@ -49,8 +59,8 @@ cdef class SchemaDummySpace(SchemaSpace):
 
 cdef class Schema:
     cdef:
-        dict schema
-        int64_t id
+        readonly dict spaces
+        readonly int id
 
     cdef SchemaSpace get_space(self, space)
     cdef SchemaSpace create_dummy_space(self, int space_id)
@@ -65,4 +75,4 @@ cdef class Schema:
     cdef Schema parse(int64_t schema_id, spaces, indexes)
 
 
-cdef list dict_to_list_fields(dict d, TntFields fields, bint default_none)
+cdef list dict_to_list_fields(dict d, Metadata metadata, bint default_none)
diff --git a/asynctnt/iproto/schema.pyx b/asynctnt/iproto/schema.pyx
index d6567f7..609c5e0 100644
--- a/asynctnt/iproto/schema.pyx
+++ b/asynctnt/iproto/schema.pyx
@@ -8,37 +8,55 @@ cimport cpython.list
 cimport cython
 from cpython.ref cimport PyObject
 
+@cython.final
+cdef class Field:
+    def __cinit__(self):
+        self.name = None
+        self.type = None
+        self.collation = None
+        self.is_nullable = None
+        self.is_autoincrement = None
+        self.span = None
+
+    def __repr__(self):  # pragma: nocover
+        return "<Field name={} type={} is_nullable={}>".format(
+            self.name, self.type, self.is_nullable
+        )
 
 @cython.final
-@cython.freelist(TNT_FIELDS_FREELIST_SIZE)
-cdef public class TntFields [object C_TntFields, type C_TntFields_Type]:
+@cython.freelist(METADATA_FREELIST_SIZE)
+cdef class Metadata:
     def __cinit__(self):
-        self._names = []
-        self._mapping = {}
+        self.fields = []
+        self.names = []
+        self.name_id_map = {}
 
-    cdef inline void add(self, uint64_t id, str name):
-        self._names.append(name)
-        self._mapping[name] = id
+    cdef inline void add(self, int id, Field field):
+        cpython.list.PyList_Append(self.fields, field)
+        cpython.list.PyList_Append(self.names, field.name)
+        cpython.dict.PyDict_SetItem(self.name_id_map, field.name, id)
 
     cdef inline str name_by_id(self, int id):
-        return <str>cpython.list.PyList_GetItem(self._names, id)
+        cdef Field field
+        field = <Field> cpython.list.PyList_GetItem(self.fields, id)
+        return field.name
 
-    cdef inline uint64_t id_by_name(self, str f) except *:
+    cdef inline int id_by_name(self, str name) except *:
         cdef:
             PyObject *fld
 
-        fld = cpython.dict.PyDict_GetItem(self._mapping, f)
+        fld = cpython.dict.PyDict_GetItem(self.name_id_map, name)
         if fld == NULL:
-            raise KeyError('Field \'{}\' not found'.format(f))
-        return <uint64_t><object>fld
+            raise KeyError('Field \'{}\' not found'.format(name))
+        return <int> <object> fld
 
     cdef inline int len(self):
-        return <int>cpython.list.PyList_GET_SIZE(self._names)
-
-    def __repr__(self):
-        return '<TntFields [{}]>'.format(', '.join(self._names))
+        return <int> cpython.list.PyList_GET_SIZE(self.fields)
 
+    def __repr__(self):  # pragma: nocover
+        return '<Metadata [fields_count={}]>'.format(self.len())
 
+@cython.final
 cdef class SchemaIndex:
     def __cinit__(self):
         self.iid = -1
@@ -46,9 +64,9 @@ cdef class SchemaIndex:
         self.name = None
         self.index_type = None
         self.unique = None
-        self.fields = None
+        self.metadata = None
 
-    def __repr__(self):
+    def __repr__(self):  # pragma: nocover
         return \
             '<{} sid={}, id={}, name={}, ' \
             'type={}, unique={}>'.format(
@@ -56,11 +74,7 @@ cdef class SchemaIndex:
                 self.sid, self.iid, self.name, self.index_type, self.unique
             )
 
-
-cdef class SchemaDummyIndex(SchemaIndex):
-    pass
-
-
+@cython.final
 cdef class SchemaSpace:
     def __cinit__(self):
         self.sid = -1
@@ -70,7 +84,7 @@ cdef class SchemaSpace:
         self.field_count = 0
         self.flags = None
 
-        self.fields = None
+        self.metadata = None
         self.indexes = {}
 
     cdef void add_index(self, SchemaIndex idx):
@@ -91,14 +105,14 @@ cdef class SchemaSpace:
 
         obj_p = cpython.dict.PyDict_GetItem(self.indexes, index)
         if obj_p is not NULL:
-            return <SchemaIndex>obj_p
+            return <SchemaIndex> obj_p
         else:
             if is_int and create_dummy:
                 logger.debug(
                     'Index %s not found in space %s/%s. Creating dummy.',
                     index, self.sid, self.name
                 )
-                idx = SchemaDummyIndex.__new__(SchemaDummyIndex)
+                idx = <SchemaIndex> SchemaIndex.__new__(SchemaIndex)
                 idx.iid = index
                 idx.sid = self.sid
                 idx.name = str(index)
@@ -111,28 +125,24 @@ cdef class SchemaSpace:
                     )
                 )
 
-    def __repr__(self):
+    def __repr__(self):  # pragma: nocover
         return '<{} id={} name={} engine={}>'.format(
             self.__class__.__name__,
             self.sid, self.name, self.engine
         )
 
-
-cdef class SchemaDummySpace(SchemaSpace):
-    pass
-
-
+@cython.final
 cdef class Schema:
-    def __cinit__(self, int64_t schema_id):
+    def __cinit__(self, int schema_id):
         self.id = schema_id
-        self.schema = {}
+        self.spaces = {}
 
     cdef SchemaSpace get_space(self, space):
         cdef PyObject *obj_p = \
-            cpython.dict.PyDict_GetItem(self.schema, space)
+            cpython.dict.PyDict_GetItem(self.spaces, space)
         if obj_p is NULL:
             return None
-        return <SchemaSpace>obj_p
+        return <SchemaSpace> obj_p
 
     cdef SchemaSpace get_or_create_space(self, space):
         cdef:
@@ -144,7 +154,7 @@ cdef class Schema:
             raise TypeError(
                 'Space must be either str or int, got: {}'.format(type(space)))
 
-        obj_p = cpython.dict.PyDict_GetItem(self.schema, space)
+        obj_p = cpython.dict.PyDict_GetItem(self.spaces, space)
         if obj_p is NULL:
             if is_str:
                 raise TarantoolSchemaError(
@@ -152,19 +162,19 @@ cdef class Schema:
                 )
             else:
                 return self.create_dummy_space(space)
-        return <SchemaSpace>obj_p
+        return <SchemaSpace> obj_p
 
     cdef SchemaSpace create_dummy_space(self, int space_id):
         cdef SchemaSpace s
         logger.debug('Space %s not found. Creating dummy.', space_id)
-        s = SchemaDummySpace.__new__(SchemaDummySpace)
+        s = <SchemaSpace> SchemaSpace.__new__(SchemaSpace)
         s.sid = space_id
         s.name = str(space_id)
-        cpython.dict.PyDict_SetItem(self.schema, space_id, s)
+        cpython.dict.PyDict_SetItem(self.spaces, space_id, s)
         return s
 
     cdef inline clear(self):
-        self.schema.clear()
+        self.spaces.clear()
 
     cdef SchemaSpace parse_space(self, space_row):
         cdef:
@@ -175,38 +185,41 @@ cdef class Schema:
 
         assert space_row is not None
 
-        sp = SchemaSpace.__new__(SchemaSpace)
-        row_len = <size_t>len(space_row)
+        sp = <SchemaSpace> SchemaSpace.__new__(SchemaSpace)
+        row_len = <size_t> len(space_row)
 
         k = 0
-        sp.sid = <int>space_row[k]
+        sp.sid = <int> space_row[k]
         k += 1
         if k < row_len:
-            sp.owner = <int>space_row[k]
+            sp.owner = <int> space_row[k]
         k += 1
         if k < row_len:
-            sp.name = <str>space_row[k]
+            sp.name = <str> space_row[k]
         k += 1
         if k < row_len:
-            sp.engine = <str>space_row[k]
+            sp.engine = <str> space_row[k]
         k += 1
         if k < row_len:
-            sp.field_count = <int>space_row[k]
+            sp.field_count = <int> space_row[k]
         k += 1
         if k < row_len:
             sp.flags = space_row[k]
         k += 1
         if k < row_len:
             # format
-            format_list = <list>space_row[k]
+            format_list = <list> space_row[k]
             if len(format_list) > 0:
-                sp.fields = TntFields.__new__(TntFields)
+                sp.metadata = <Metadata> Metadata.__new__(Metadata)
                 for i in range(len(format_list)):
                     field_id = i
-                    field_name = format_list[i]['name']
-                    # field_type = format_list[i]['type']
+                    field = <Field> Field.__new__(Field)
+
+                    field.name = format_list[i]['name']
+                    field.type = format_list[i]['type']
+                    field.is_nullable = format_list[i].get('is_nullable')
 
-                    sp.fields.add(field_id, field_name)
+                    sp.metadata.add(field_id, field)
 
         return sp
 
@@ -220,56 +233,69 @@ cdef class Schema:
 
         assert index_row is not None
 
-        idx = SchemaIndex.__new__(SchemaIndex)
-        idx.sid = <int>index_row[0]
-        idx.iid = <int>index_row[1]
-        idx.name = <str>index_row[2]
-        idx.index_type = <str>index_row[3]
-        idx.unique = <object>index_row[4]
-        idx.fields = None
+        idx = <SchemaIndex> SchemaIndex.__new__(SchemaIndex)
+        idx.sid = <int> index_row[0]
+        idx.iid = <int> index_row[1]
+        idx.name = <str> index_row[2]
+        idx.index_type = <str> index_row[3]
+        idx.unique = <object> index_row[4]
+        idx.metadata = None
 
         sp = self.get_space(idx.sid)
         if sp is None:
             raise TarantoolSchemaError(
                 'Space with id {} not found'.format(idx.sid))
 
-        if sp.fields is not None:
+        if sp.metadata is not None:
             parts = index_row[5]
+            idx.metadata = <Metadata> Metadata.__new__(Metadata)
+
             if isinstance(parts, (list, tuple)):
-                idx.fields = TntFields.__new__(TntFields)
                 for part in parts:
+                    field = <Field> Field.__new__(Field)
+                    field.name = ''
+
                     if isinstance(part, (list, tuple)):
                         assert len(part) == 2, 'Part len must be 2'
                         field_id = part[0]
-                        field_type = part[1]
+                        field.type = part[1]
                     elif isinstance(part, dict):
                         field_id = part['field']
-                        field_type = part['type']
+                        field.type = part['type']
                         # TODO: add is_nullable and collation if we really need
                         # TODO: it in a python driver
-
-                    if field_id < sp.fields.len():
-                        f = sp.fields.name_by_id(field_id)
-                        idx.fields.add(field_id, f)
+                    else:
+                        raise TypeError(
+                            "unexpected type of part: {}. "
+                            "must be either dict or list/tuple".format(
+                                type(part)
+                            )
+                        )
+
+                    if field_id < sp.metadata.len():
+                        field.name = sp.metadata.name_by_id(field_id)
                     else:
                         logger.debug(
                             'Field #%d of space %s is not '
                             'in space format definition', field_id, sp.name)
-                        idx.fields.add(field_id, '')
+
+                    idx.metadata.add(field_id, field)
             else:
-                idx.fields = TntFields.__new__(TntFields)
-                for i in range(index_row[5]):
+                for i in range(parts):
+                    field = <Field> Field.__new__(Field)
+
                     field_id = index_row[5 + 1 + i * 2]
-                    field_type = index_row[5 + 2 + i * 2]
+                    field.type = index_row[5 + 2 + i * 2]
+                    field.name = ''
 
-                    if field_id < sp.fields.len():
-                        f = sp.fields.name_by_id(field_id)
-                        idx.fields.add(field_id, f)
+                    if field_id < sp.metadata.len():
+                        field.name = sp.metadata.name_by_id(field_id)
                     else:
                         logger.debug(
                             'Field #%d of space %s is not '
                             'in space format definition', field_id, sp.name)
-                        idx.fields.add(field_id, '')
+
+                    idx.metadata.add(field_id, field)
 
         return idx
 
@@ -280,25 +306,24 @@ cdef class Schema:
             SchemaSpace sp
             SchemaIndex idx
 
-        s = Schema.__new__(Schema, schema_id)
+        s = <Schema> Schema.__new__(Schema, <int> schema_id)
         for space_row in spaces:
             sp = s.parse_space(space_row)
-            cpython.dict.PyDict_SetItem(s.schema, sp.sid, sp)
+            cpython.dict.PyDict_SetItem(s.spaces, sp.sid, sp)
             if sp.name:
-                cpython.dict.PyDict_SetItem(s.schema, sp.name, sp)
+                cpython.dict.PyDict_SetItem(s.spaces, sp.name, sp)
 
         for index_row in indexes:
             idx = s.parse_index(index_row)
-            sp = s.schema[idx.sid]
+            sp = s.spaces[idx.sid]
             sp.add_index(idx)
 
         return s
 
-    def __repr__(self):
-        return '<Schema spaces={}>'.format(len(self.schema))
-
+    def __repr__(self):  # pragma: nocover
+        return '<Schema spaces={}>'.format(len(self.spaces))
 
-cdef list dict_to_list_fields(dict d, TntFields fields, bint default_none):
+cdef list dict_to_list_fields(dict d, Metadata metadata, bint default_none):
     cdef:
         list tpl
         dict used
@@ -308,18 +333,18 @@ cdef list dict_to_list_fields(dict d, TntFields fields, bint default_none):
         int field_id
         PyObject *obj_p
 
-    assert fields is not None
+    assert metadata is not None
     assert d is not None
 
     tpl = []
-    used_count = <int32_t>cpython.dict.PyDict_Size(d)
+    used_count = <int32_t> cpython.dict.PyDict_Size(d)
 
-    for field_id in range(fields.len()):
-        field_name = fields.name_by_id(field_id)
+    for field_id in range(metadata.len()):
+        field_name = metadata.name_by_id(field_id)
 
         obj_p = cpython.dict.PyDict_GetItem(d, field_name)
         if obj_p is not NULL:
-            value = <object>obj_p
+            value = <object> obj_p
             used_count -= 1
             tpl.append(value)
         else:
@@ -329,12 +354,12 @@ cdef list dict_to_list_fields(dict d, TntFields fields, bint default_none):
     # Warn user if he used any of unknown fields
     if used_count != 0:
         used = {}
-        for field_id in range(fields.len()):
-            field_name = fields.name_by_id(field_id)
+        for field_id in range(metadata.len()):
+            field_name = metadata.name_by_id(field_id)
 
-            if <bint>cpython.dict.PyDict_Contains(d, field_name):
+            if <bint> cpython.dict.PyDict_Contains(d, field_name):
                 used[field_name] = None
-        if <bint>cpython.dict.PyDict_Contains(d, ''):
+        if <bint> cpython.dict.PyDict_Contains(d, ''):
             used[''] = None
 
         for f in d:
diff --git a/asynctnt/iproto/tarantool.pxd b/asynctnt/iproto/tarantool.pxd
index 50c2015..fe1958d 100644
--- a/asynctnt/iproto/tarantool.pxd
+++ b/asynctnt/iproto/tarantool.pxd
@@ -7,6 +7,7 @@ cdef enum iproto_header_key:
     IPROTO_SCHEMA_VERSION = 0x05
     IPROTO_SERVER_VERSION = 0x06
     IPROTO_GROUP_ID = 0x07
+    IPROTO_STREAM_ID=0x0a
 
 
 cdef enum iproto_key:
@@ -31,40 +32,58 @@ cdef enum iproto_key:
     IPROTO_OPTIONS = 0x2b
 
     IPROTO_DATA = 0x30
-    IPROTO_ERROR = 0x31
+    IPROTO_ERROR_24 = 0x31
     IPROTO_METADATA = 0x32
+    IPROTO_BIND_METADATA = 0x33
+    IPROTO_BIND_COUNT = 0x34
 
     IPROTO_SQL_TEXT = 0x40
     IPROTO_SQL_BIND = 0x41
     IPROTO_SQL_INFO = 0x42
+    IPROTO_STMT_ID = 0x43
+
+    IPROTO_ERROR = 0x52
+    IPROTO_VERSION = 0x54
+    IPROTO_FEATURES = 0x55
+    IPROTO_TIMEOUT = 0x56
+    IPROTO_TXN_ISOLATION = 0x59
+
+    IPROTO_CHUNK = 0x80
 
 
 cdef enum iproto_metadata_key:
-    IPROTO_FIELD_NAME = 0
-    IPROTO_FIELD_TYPE = 1
+    IPROTO_FIELD_NAME = 0x00
+    IPROTO_FIELD_TYPE = 0x01
+    IPROTO_FIELD_COLL = 0x02
+    IPROTO_FIELD_IS_NULLABLE = 0x03
+    IPROTO_FIELD_IS_AUTOINCREMENT = 0x04
+    IPROTO_FIELD_SPAN = 0x05
 
 
 cdef enum iproto_sql_info_key:
-    SQL_INFO_ROW_COUNT = 0
-    SQL_INFO_AUTOINCREMENT_IDS = 1
+    SQL_INFO_ROW_COUNT = 0x00
+    SQL_INFO_AUTOINCREMENT_IDS = 0x01
 
 
 cdef enum iproto_type:
-    IPROTO_OK = 0
-    IPROTO_SELECT = 1
-    IPROTO_INSERT = 2
-    IPROTO_REPLACE = 3
-    IPROTO_UPDATE = 4
-    IPROTO_DELETE = 5
-    IPROTO_CALL_16 = 6
-    IPROTO_AUTH = 7
-    IPROTO_EVAL = 8
-    IPROTO_UPSERT = 9
-    IPROTO_CALL = 10
-    IPROTO_EXECUTE = 11
-    IPROTO_PING = 64
-
-    IPROTO_CHUNK = 128
+    IPROTO_SELECT = 0x01
+    IPROTO_INSERT = 0x02
+    IPROTO_REPLACE = 0x03
+    IPROTO_UPDATE = 0x04
+    IPROTO_DELETE = 0x05
+    IPROTO_CALL_16 = 0x06
+    IPROTO_AUTH = 0x07
+    IPROTO_EVAL = 0x08
+    IPROTO_UPSERT = 0x09
+    IPROTO_CALL = 0x0a
+    IPROTO_EXECUTE = 0x0b
+    IPROTO_PREPARE = 0x0d
+    IPROTO_BEGIN = 0x0e
+    IPROTO_COMMIT = 0x0f
+    IPROTO_ROLLBACK = 0x10
+    IPROTO_PING = 0x40
+    IPROTO_ID = 0x49
+
 
 cdef enum iproto_update_operation:
     IPROTO_OP_ADD = b'+'
@@ -76,3 +95,29 @@ cdef enum iproto_update_operation:
     IPROTO_OP_INSERT = b'!'
     IPROTO_OP_ASSIGN = b'='
     IPROTO_OP_SPLICE = b':'
+
+
+cdef enum mp_extension_type:
+    MP_UNKNOWN_EXTENSION = 0
+    MP_DECIMAL = 1
+    MP_UUID = 2
+    MP_ERROR = 3
+    MP_DATETIME = 4
+
+cdef enum iproto_features:
+    IPROTO_FEATURE_STREAMS = 0
+    IPROTO_FEATURE_TRANSACTIONS = 1
+    IPROTO_FEATURE_ERROR_EXTENSION = 2
+    IPROTO_FEATURE_WATCHERS = 3
+
+cdef enum iproto_error_fields:
+    MP_ERROR_STACK = 0x00
+
+cdef enum iproto_error_stack_fields:
+    MP_ERROR_TYPE = 0x00
+    MP_ERROR_FILE = 0x01
+    MP_ERROR_LINE = 0x02
+    MP_ERROR_MESSAGE = 0x03
+    MP_ERROR_ERRNO = 0x04
+    MP_ERROR_ERRCODE = 0x05
+    MP_ERROR_FIELDS = 0x06
diff --git a/asynctnt/iproto/ttuple.pyx b/asynctnt/iproto/ttuple.pyx
new file mode 100644
index 0000000..f74109c
--- /dev/null
+++ b/asynctnt/iproto/ttuple.pyx
@@ -0,0 +1,1 @@
+TarantoolTuple = <object> tupleobj.AtntTuple_InitTypes()
diff --git a/asynctnt/iproto/tupleobj/tupleobj.c b/asynctnt/iproto/tupleobj/tupleobj.c
index 988c834..f720f3f 100644
--- a/asynctnt/iproto/tupleobj/tupleobj.c
+++ b/asynctnt/iproto/tupleobj/tupleobj.c
@@ -17,16 +17,16 @@ static int numfree[AtntTuple_MAXSAVESIZE];
 
 
 PyObject *
-AtntTuple_New(PyObject *fields, Py_ssize_t size)
+AtntTuple_New(PyObject *metadata, Py_ssize_t size)
 {
     AtntTupleObject *o;
     Py_ssize_t i;
 
-    if (fields == Py_None) {
-        fields = NULL;
+    if (metadata == Py_None) {
+        metadata = NULL;
     }
 
-    if (size < 0 || (fields != NULL && !C_TntFields_CheckExact(fields))) {
+    if (size < 0 || (metadata != NULL && !C_Metadata_CheckExact(metadata))) {
         PyErr_BadInternalCall();
         return NULL;
     }
@@ -52,8 +52,8 @@ AtntTuple_New(PyObject *fields, Py_ssize_t size)
         o->ob_item[i] = NULL;
     }
 
-    Py_XINCREF(fields);
-    o->fields = (struct C_TntFields *) fields;
+    Py_XINCREF(metadata);
+    o->metadata = (struct C_Metadata *) metadata;
     o->self_hash = -1;
     PyObject_GC_Track(o);
     return (PyObject *) o;
@@ -70,7 +70,7 @@ ttuple_dealloc(AtntTupleObject *o)
 
     o->self_hash = -1;
 
-    Py_CLEAR(o->fields);
+    Py_CLEAR(o->metadata);
 
     Py_TRASHCAN_SAFE_BEGIN(o)
     if (len > 0) {
@@ -100,7 +100,7 @@ ttuple_traverse(AtntTupleObject *o, visitproc visit, void *arg)
 {
     Py_ssize_t i;
 
-    Py_VISIT(o->fields);
+    Py_VISIT(o->metadata);
 
     for (i = Py_SIZE(o); --i >= 0;) {
         if (o->ob_item[i] != NULL) {
@@ -261,7 +261,7 @@ ttuple_item(AtntTupleObject *o, Py_ssize_t i)
 static int
 ttuple_item_by_name(AtntTupleObject *o, PyObject *item, PyObject **result)
 {
-    if (o->fields == NULL) {
+    if (o->metadata == NULL) {
         goto noitem;
     }
 
@@ -269,7 +269,7 @@ ttuple_item_by_name(AtntTupleObject *o, PyObject *item, PyObject **result)
     Py_ssize_t i;
     PyObject *value;
 
-    mapped = PyObject_GetItem(o->fields->_mapping, item);
+    mapped = PyObject_GetItem(o->metadata->name_id_map, item);
     if (mapped == NULL) {
         goto noitem;
     }
@@ -371,8 +371,8 @@ ttuple_repr(AtntTupleObject *v)
         return PyUnicode_FromString("<TarantoolTuple>");
     }
 
-    if (v->fields != NULL) {
-        keys_iter = PyObject_GetIter(v->fields->_names);
+    if (v->metadata != NULL) {
+        keys_iter = PyObject_GetIter(v->metadata->names);
         if (keys_iter == NULL) {
             return NULL;
         }
@@ -485,12 +485,12 @@ ttuple_keys(PyObject *o, PyObject *args)
         return NULL;
     }
 
-    if (((AtntTupleObject *) o)->fields == NULL) {
+    if (((AtntTupleObject *) o)->metadata == NULL) {
         PyErr_SetString(PyExc_ValueError, "No keys for this tuple");
         return NULL;
     }
 
-    return PyObject_GetIter(((AtntTupleObject *) o)->fields->_names);
+    return PyObject_GetIter(((AtntTupleObject *) o)->metadata->names);
 }
 
 
@@ -502,7 +502,7 @@ ttuple_items(PyObject *o, PyObject *args)
         return NULL;
     }
 
-    if (((AtntTupleObject *) o)->fields == NULL) {
+    if (((AtntTupleObject *) o)->metadata == NULL) {
         PyErr_SetString(PyExc_ValueError, "No keys for this tuple");
         return NULL;
     }
@@ -519,12 +519,12 @@ ttuple_contains(AtntTupleObject *o, PyObject *arg)
         return -1;
     }
 
-    if (o->fields == NULL) {
+    if (o->metadata == NULL) {
         PyErr_SetString(PyExc_ValueError, "No keys for this tuple");
         return 0;
     }
 
-    return PySequence_Contains(o->fields->_mapping, arg);
+    return PySequence_Contains(o->metadata->name_id_map, arg);
 }
 
 
@@ -898,7 +898,7 @@ ttuple_new_items_iter(PyObject *seq)
         return NULL;
     }
 
-    key_iter = PyObject_GetIter(((AtntTupleObject*)seq)->fields->_names);
+    key_iter = PyObject_GetIter(((AtntTupleObject*)seq)->metadata->names);
     if (key_iter == NULL) {
         return NULL;
     }
diff --git a/asynctnt/iproto/tupleobj/tupleobj.h b/asynctnt/iproto/tupleobj/tupleobj.h
index 21d3b6c..63dba71 100644
--- a/asynctnt/iproto/tupleobj/tupleobj.h
+++ b/asynctnt/iproto/tupleobj/tupleobj.h
@@ -4,6 +4,10 @@
 #include "Python.h"
 #include "protocol.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 /* Largest ttuple to save on free list */
 #define AtntTuple_MAXSAVESIZE 20
 
@@ -21,7 +25,7 @@ typedef struct {
 typedef struct {
     PyObject_VAR_HEAD
     Py_hash_t self_hash;
-    struct C_TntFields *fields;
+    struct C_Metadata *metadata;
     PyObject *ob_item[1];
 
     /* ob_item contains space for 'ob_size' elements.
@@ -38,7 +42,7 @@ extern PyTypeObject AtntTupleItems_Type;
 extern PyTypeObject AtntTupleDesc_Type;
 
 #define AtntTuple_CheckExact(o) (Py_TYPE(o) == &AtntTuple_Type)
-#define C_TntFields_CheckExact(o) (Py_TYPE(o) == &C_TntFields_Type)
+#define C_Metadata_CheckExact(o) (Py_TYPE(o) == &C_Metadata_Type)
 
 #define AtntTuple_SET_ITEM(op, i, v) \
             (((AtntTupleObject *)(op))->ob_item[i] = v)
@@ -48,4 +52,8 @@ extern PyTypeObject AtntTupleDesc_Type;
 PyTypeObject *AtntTuple_InitTypes(void);
 PyObject *AtntTuple_New(PyObject *, Py_ssize_t);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
diff --git a/asynctnt/iproto/xd.pxd b/asynctnt/iproto/xd.pxd
new file mode 100644
index 0000000..e11b50b
--- /dev/null
+++ b/asynctnt/iproto/xd.pxd
@@ -0,0 +1,2 @@
+cdef extern from "../../third_party/xd.h":
+    char * xd(char *data, size_t size)
diff --git a/asynctnt/prepared.py b/asynctnt/prepared.py
new file mode 100644
index 0000000..18058ec
--- /dev/null
+++ b/asynctnt/prepared.py
@@ -0,0 +1,93 @@
+from typing import List, Optional, Union, Dict, Any, TYPE_CHECKING
+
+from .iproto import protocol
+
+if TYPE_CHECKING:  # pragma: nocover
+    from .api import Api
+
+
+class PreparedStatement:
+    __slots__ = (
+        '_api', '_query', '_stmt_id',
+        '_params', '_params_count'
+    )
+
+    def __init__(self, api: 'Api', query: str):
+        self._api = api
+        self._query = query
+        self._stmt_id = None
+        self._params = None
+        self._params_count = 0
+
+    @property
+    def id(self) -> int:
+        """
+            Prepared statement id
+        """
+        return self._stmt_id
+
+    @property
+    def params_count(self) -> int:
+        """
+            Bound params count
+        """
+        return self._params_count
+
+    @property
+    def params(self) -> Optional[protocol.Metadata]:
+        """
+            Bound params metadata
+        """
+        return self._params
+
+    async def prepare(self, timeout: float = -1.0) -> int:
+        """
+            Prepare statement
+
+        :param timeout: request timeout
+        :return: prepared statement id
+        """
+        resp = await self._api.prepare_iproto(self._query, timeout=timeout)
+        self._stmt_id = resp.stmt_id
+        self._params = resp.params
+        self._params_count = resp.params_count
+        return self._stmt_id
+
+    async def execute(self,
+                      args: Optional[List[Union[Dict[str, Any], Any]]] = None,
+                      *,
+                      parse_metadata: bool = True,
+                      timeout: float = -1.0) -> protocol.Response:
+        """
+            Execute this prepared statement with specified args
+        :param args: arguments list
+        :param parse_metadata: whether to parse response metadata or not
+        :param timeout: request timeout
+        """
+        return await self._api.execute(
+            query=self._stmt_id,
+            args=args,
+            parse_metadata=parse_metadata,
+            timeout=timeout,
+        )
+
+    async def unprepare(self, timeout: float = -1.0):
+        """
+            Unprepare current prepared statement
+        :param timeout: request timeout
+        """
+        await self._api.unprepare_iproto(self._stmt_id, timeout=timeout)
+        self._stmt_id = None
+
+    async def __aenter__(self):
+        """
+            If used as a Context Manager `prepare()` and `unprepare()` methods
+            are called automatically
+        """
+        if self._stmt_id is None:
+            await self.prepare()
+        return self
+
+    async def __aexit__(self, exc_type, exc_val, exc_tb):
+        if self._stmt_id is not None:
+            await self.unprepare()
diff --git a/asynctnt/stream.py b/asynctnt/stream.py
new file mode 100644
index 0000000..ef56da7
--- /dev/null
+++ b/asynctnt/stream.py
@@ -0,0 +1,32 @@
+from .api import Api
+
+
+class Stream(Api):
+    def __init__(self):
+        super().__init__()
+
+    @property
+    def stream_id(self) -> int:
+        """
+            Current stream is
+        """
+        return self._db.stream_id
+
+    async def __aenter__(self):
+        """
+            If used as Context Manager `begin()` and `commit()`/`rollback()`
+            are called automatically
+        :return:
+        """
+        await self.begin()
+        return self
+
+    async def __aexit__(self, exc_type, exc_val, exc_tb):
+        """
+            Normally `commit()` is called on context manager exit, but
+            in the case of exception `rollback()` is called
+        """
+        if exc_type and exc_val:
+            await self.rollback()
+        else:
+            await self.commit()
diff --git a/asynctnt/types.py b/asynctnt/types.py
new file mode 100644
index 0000000..d150337
--- /dev/null
+++ b/asynctnt/types.py
@@ -0,0 +1,10 @@
+import asyncio
+from typing import Union, Awaitable, List, Any, Tuple, Dict
+
+from asynctnt.iproto import protocol
+
+MethodRet = Union[Awaitable[protocol.Response], asyncio.Future]
+SpaceType = Union[str, int]
+IndexType = Union[str, int]
+KeyType = Union[List[Any], Tuple]
+TupleType = Union[List[Any], Tuple, Dict[str, Any]]
diff --git a/asynctnt/utils.py b/asynctnt/utils.py
index 2a19135..873e83a 100644
--- a/asynctnt/utils.py
+++ b/asynctnt/utils.py
@@ -7,7 +7,7 @@
 
 if PY_37:
     __get_running_loop = asyncio.get_running_loop
-else:
+else:  # pragma: nocover
     def __get_running_loop() -> asyncio.AbstractEventLoop:
         loop = asyncio.get_event_loop()
         if not loop.is_running():
@@ -16,7 +16,7 @@ def __get_running_loop() -> asyncio.AbstractEventLoop:
 
 
 def get_running_loop(loop_arg: Optional[asyncio.AbstractEventLoop] = None):
-    if loop_arg is not None:
+    if loop_arg is not None:  # pragma: nocover
         warnings.warn(
             'loop argument is deprecated and is dropped',
             DeprecationWarning,
diff --git a/temp/superbench.py b/bench/benchmark.py
similarity index 91%
rename from temp/superbench.py
rename to bench/benchmark.py
index 4dd638c..4a622a3 100644
--- a/temp/superbench.py
+++ b/bench/benchmark.py
@@ -29,10 +29,10 @@ def main():
         ['select', [512]],
         ['replace', [512, [2, 'hhhh']]],
         ['update', [512, [2], [(':', 1, 1, 3, 'yo!')]]],
-        # ['sql', ['select 1 as a, 2 as b'], dict(parse_metadata=False)],
+        ['execute', ['select 1 as a, 2 as b'], dict(parse_metadata=False)],
     ]
 
-    for use_uvloop in [True, ]:
+    for use_uvloop in [True]:
         if use_uvloop:
             import uvloop
             asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
@@ -79,20 +79,20 @@ async def bulk_f():
         name, method, elapsed, n / elapsed.total_seconds()))
 
 
-async def create_asynctnt(loop):
+async def create_asynctnt():
     import asynctnt
     conn = asynctnt.Connection(host=HOST,
                                port=PORT,
                                username=USERNAME,
                                password=PASSWORD,
                                reconnect_timeout=1,
-                               fetch_schema=True,
-                               auto_refetch_schema=True)
+                               fetch_schema=False,
+                               auto_refetch_schema=False)
     await conn.connect()
     return conn
 
 
-async def create_aiotarantool(loop):
+async def create_aiotarantool():
     import aiotarantool
     conn = aiotarantool.connect(HOST, PORT,
                                 user=USERNAME,
diff --git a/temp/init.lua b/bench/init.lua
similarity index 100%
rename from temp/init.lua
rename to bench/init.lua
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
new file mode 120000
index 0000000..04c99a5
--- /dev/null
+++ b/docs/CHANGELOG.md
@@ -0,0 +1,1 @@
+../CHANGELOG.md
\ No newline at end of file
diff --git a/docs/api.rst b/docs/api.rst
deleted file mode 100644
index 583f7f6..0000000
--- a/docs/api.rst
+++ /dev/null
@@ -1,63 +0,0 @@
-.. _asynctnt-api:
-
-=============
-API Reference
-=============
-
-.. module:: asynctnt
-    :synopsis: A fast Tarantool Database connector for Python/asyncio.
-
-.. currentmodule:: asynctnt
-
-
-Connection
-==========
-
-.. autofunction:: asynctnt.connect
-
-
-.. autoclass:: asynctnt.Connection
-   :members:
-
-   .. automethod:: __init__
-
-
-PushIterator
-============
-
-.. autoclass:: asynctnt.PushIterator
-   :members:
-
-   .. automethod:: __init__
-
-Response
-========
-
-.. autoclass:: asynctnt.Response
-   :members:
-
-
-Iterator
-========
-
-.. autoclass:: asynctnt.Iterator
-   :members:
-
-
-Exceptions
-==========
-
-.. autoclass:: asynctnt.exceptions.TarantoolError
-   :members:
-
-.. autoclass:: asynctnt.exceptions.TarantoolSchemaError
-   :members:
-
-.. autoclass:: asynctnt.exceptions.TarantoolDatabaseError
-   :members:
-
-.. autoclass:: asynctnt.exceptions.TarantoolNotConnectedError
-   :members:
-
-.. autoclass:: asynctnt.exceptions.ErrorCode
-   :members:
diff --git a/docs/conf.py b/docs/conf.py
index cb39b2e..f2765ff 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -35,42 +35,46 @@ def find_version():
 
 _ver = find_version()
 
-
 # -- General configuration ------------------------------------------------
 
 extensions = ['sphinx.ext.autodoc',
               'sphinx.ext.viewcode',
               'sphinx.ext.githubpages',
-              'sphinxcontrib.asyncio']
+              'sphinxcontrib.asyncio',
+              'myst_parser',
+              'sphinx_autodoc_typehints',
+              'sphinx_rtd_theme',
+              'autoapi.extension'
+              ]
 
 templates_path = ['_templates']
-source_suffix = '.rst'
+source_suffix = {
+    '.rst': 'restructuredtext',
+    '.md': 'markdown',
+}
 master_doc = 'index'
 
 project = 'asynctnt'
-copyright = '2017, igorcoding'
+copyright = '2022, igorcoding'
 author = 'igorcoding'
 
 version = _ver
 release = _ver
 
-language = None
+language = "en"
 exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
 pygments_style = 'sphinx'
 todo_include_todos = False
 
-
 # -- Options for HTML output ----------------------------------------------
 
 html_theme = "sphinx_rtd_theme"
 html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
 html_static_path = ['_static']
 
-
 # -- Options for HTMLHelp output ------------------------------------------
 htmlhelp_basename = 'asynctntdoc'
 
-
 # -- Options for LaTeX output ---------------------------------------------
 
 latex_elements = {
@@ -99,7 +103,6 @@ def find_version():
      'igorcoding', 'manual'),
 ]
 
-
 # -- Options for manual page output ---------------------------------------
 
 # One entry per manual page. List of tuples
@@ -109,7 +112,6 @@ def find_version():
      [author], 1)
 ]
 
-
 # -- Options for Texinfo output -------------------------------------------
 
 # Grouping the document tree into Texinfo files. List of tuples
@@ -120,3 +122,16 @@ def find_version():
      author, 'asynctnt', 'One line description of project.',
      'Miscellaneous'),
 ]
+
+always_document_param_types = True
+typehints_defaults = 'comma'
+
+autoapi_type = 'python'
+autoapi_dirs = ['../asynctnt']
+
+html_context = {
+    'display_github': True,
+    'github_user': 'igorcoding',
+    'github_repo': 'asynctnt',
+    'github_version': 'v2/docs/',
+}
diff --git a/docs/examples.md b/docs/examples.md
new file mode 100644
index 0000000..480861f
--- /dev/null
+++ b/docs/examples.md
@@ -0,0 +1,67 @@
+# Examples
+
+## Basic Usage
+
+```lua
+box.cfg {
+    listen = '127.0.0.1:3301'
+}
+
+box.once('v1', function()
+    box.schema.user.grant('guest', 'read,write,execute', 'universe')
+
+    local s = box.schema.create_space('tester')
+    s:create_index('primary')
+    s:format({
+        { name = 'id', type = 'unsigned' },
+        { name = 'name', type = 'string' },
+    })
+end)
+```
+
+Python code:
+
+```python
+import asyncio
+import asynctnt
+
+
+async def main():
+    conn = asynctnt.Connection(host='127.0.0.1', port=3301)
+    await conn.connect()
+
+    for i in range(1, 11):
+        await conn.insert('tester', [i, 'hello{}'.format(i)])
+
+    data = await conn.select('tester', [])
+    first_tuple = data[0]
+    print('tuple:', first_tuple)
+    print(f'tuple[0]: {first_tuple[0]}; tuple["id"]: {first_tuple["id"]}')
+    print(f'tuple[1]: {first_tuple[1]}; tuple["name"]: {first_tuple["name"]}')
+
+    await conn.disconnect()
+
+asyncio.run(main())
+```
+
+Stdout:
+```
+tuple: <TarantoolTuple id=1 name='hello1'>
+tuple[0]: 1; tuple["id"]: 1
+tuple[1]: hello1; tuple["name"]: hello1
+```
+
+## Using Connection context manager
+
+```python
+import asyncio
+import asynctnt
+
+
+async def main():
+    async with asynctnt.Connection(port=3301) as conn:
+        res = await conn.call('box.info')
+        print(res.body)
+
+asyncio.run(main())
+```
diff --git a/docs/examples.rst b/docs/examples.rst
deleted file mode 100644
index e2d798c..0000000
--- a/docs/examples.rst
+++ /dev/null
@@ -1,134 +0,0 @@
-.. _asynctnt-examples:
-
-Examples
-========
-Basic Usage
------------
-
-Tarantool config:
-
-.. code:: lua
-
-    box.cfg {
-        listen = '127.0.0.1:3301'
-    }
-
-    box.once('v1', function()
-        box.schema.user.grant('guest', 'read,write,execute', 'universe')
-
-        local s = box.schema.create_space('tester')
-        s:create_index('primary')
-        s:format({
-            { name = 'id', type = 'unsigned' },
-            { name = 'name', type = 'string' },
-        })
-    end)
-
-Python code:
-
-.. code:: python
-
-    import asyncio
-    import asynctnt
-
-
-    async def main():
-        conn = asynctnt.Connection(host='127.0.0.1', port=3301)
-        await conn.connect()
-
-        for i in range(1, 11):
-            await conn.insert('tester', [i, 'hello{}'.format(i)])
-
-        data = await conn.select('tester', [])
-        first_tuple = data[0]
-        print('tuple:', first_tuple)
-        print(f'tuple[0]: {first_tuple[0]}; tuple["id"]: {first_tuple["id"]}')
-        print(f'tuple[1]: {first_tuple[1]}; tuple["name"]: {first_tuple["name"]}')
-
-        await conn.disconnect()
-
-    asyncio.run(main())
-
-Stdout:
-
-::
-
-    tuple: <TarantoolTuple id=1 name='hello1'>
-    tuple[0]: 1; tuple["id"]: 1
-    tuple[1]: hello1; tuple["name"]: hello1
-
-
-Using SQL
----------
-
-Tarantool config:
-
-.. code:: lua
-
-    box.cfg {
-        listen = '127.0.0.1:3301'
-    }
-
-    box.once('v1', function()
-        box.schema.user.grant('guest', 'read,write,execute', 'universe')
-
-        box.execute([[
-            create table users (
-                id int primary key autoincrement,
-                name text
-            )
-        ]])
-    end)
-
-
-Python code:
-
-.. code:: python
-
-    import asyncio
-    import asynctnt
-
-
-    async def main():
-        conn = asynctnt.Connection(host='127.0.0.1', port=3301)
-        await conn.connect()
-
-        await conn.sql("insert into users (name) values ('James Bond')")
-        resp = await conn.sql("insert into users (name) values ('Ethan Hunt')")
-
-        # get value of auto incremented primary key
-        print(resp.autoincrement_ids)
-
-        data = await conn.sql('select * from users')
-
-        for row in data:
-            print(row)
-
-        await conn.disconnect()
-
-    asyncio.run(main())
-
-
-Stdout:
-
-.. code::
-
-    <TarantoolTuple ID=1 NAME='James Bond'>
-    <TarantoolTuple ID=2 NAME='Ethan Hunt'>
-
-
-Using Connection context manager
---------------------------------
-
-.. code:: python
-
-    import asyncio
-    import asynctnt
-
-
-    async def main():
-        async with asynctnt.Connection(port=3301) as conn:
-            res = await conn.call('box.info')
-            print(res.body)
-
-    asyncio.run(main())
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..175cc4a
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,15 @@
+# asynctnt
+
+```{toctree}
+:maxdepth: 2
+
+overview
+installation
+examples
+sql
+metadata
+pushes
+streams
+mpext
+CHANGELOG
+```
diff --git a/docs/index.rst b/docs/index.rst
deleted file mode 100644
index 034cf08..0000000
--- a/docs/index.rst
+++ /dev/null
@@ -1,69 +0,0 @@
-.. _asynctnt-intro:
-
-========
-asynctnt
-========
-
-.. image:: https://travis-ci.org/igorcoding/asynctnt.svg?branch=master
-    :target: https://travis-ci.org/igorcoding/asynctnt
-
-.. image:: https://img.shields.io/pypi/v/asynctnt.svg
-    :target: https://pypi.python.org/pypi/asynctnt
-
-
-asynctnt is a high-performance `Tarantool <https://tarantool.org/>`__
-database connector library for Python/asyncio. It was highly inspired by
-`asyncpg <https://github.com/MagicStack/asyncpg>`__ module.
-asynctnt requires Python 3.5 or later and is supported for Tarantool
-versions 1.6+.
-
-Key features
-------------
--  Support for all of the basic requests that Tarantool supports. This includes:
-   `insert`, `select`, `update`, `upsert`, `eval`, `sql` (for Tarantool 2.x),
-   `call` and `call16`.
-
-   *Note: For the difference between `call16` and `call` please refer to
-   Tarantool documentation.*
--  **Schema fetching** on connection establishment, so you can use spaces and
-   indexes names rather than their ids.
--  Schema **auto refetching**. Tarantool has an option to check if "your" schema
-   is up to date, and if not - returns an error. If such an error occurs on any
-   request - new schema is refetched and the initial request is resent.
--  **Auto reconnect**. If connection is lost for some reason - asynctnt will
-   start automatic reconnection procedure (with authorization and schema
-   fetching, of course).
--  Ability to use **dicts for tuples** with field names as keys in DML requests
-   (select, insert, replace, delete, update, upsert). This is possible only
-   if space.format is specified in Tarantool. Field names can also be used
-   in update operations instead of field numbers. Moreover, tuples are decoded
-   into the special structures that can act either as `tuple`s or by `dict`s with
-   the appropriate API.
--  All requests support specification of `timeout` value, so if request is
-   executed for too long, asyncio.TimeoutError is raised. It drastically
-   simplifies your code, as you don't need to use `asyncio.wait_for(...)`
-   stuff anymore.
-
-
-License
--------
-asynctnt is developed and distributed under the Apache 2.0 license.
-
-
-.. toctree::
-   :maxdepth: 2
-   :caption: Contents:
-
-   installation
-   examples
-   pushes
-   api
-
-
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
diff --git a/docs/installation.md b/docs/installation.md
new file mode 100644
index 0000000..9cb66c7
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,16 @@
+# Installation
+
+To install asynctnt through **pip** run:
+
+```bash
+$ pip install asynctnt
+```
+
+
+# Running tests
+
+To execute the testsuite simply run:
+
+```bash
+$ make test
+```
diff --git a/docs/installation.rst b/docs/installation.rst
deleted file mode 100644
index 736ae50..0000000
--- a/docs/installation.rst
+++ /dev/null
@@ -1,21 +0,0 @@
-.. _asynctnt-installation:
-
-
-Installation
-============
-
-To install asynctnt through **pip** run:
-
-.. code-block:: bash
-
-    $ pip install asynctnt
-
-
-Running tests
--------------
-
-To execute the testsuite simply run:
-
-.. code-block:: bash
-
-    $ make test
diff --git a/docs/metadata.md b/docs/metadata.md
new file mode 100644
index 0000000..015d6b7
--- /dev/null
+++ b/docs/metadata.md
@@ -0,0 +1,16 @@
+# Metadata
+
+`asynctnt` fetches schema from Tarantool server in order to provide ability to use
+space and index names in the CRUD requests, as well as field names of the tuples.
+You can access this metadata in the `Connection` object directly. This schema may be
+refreshed if schema is changed in Tarantool.
+
+```python
+import asynctnt
+
+conn = await asynctnt.connect()
+
+print('space id', conn.schema.spaces['_space'].sid)
+print('space engine', conn.schema.spaces['_space'].engine)
+print('space format fields', conn.schema.spaces['_space'].metadata.fields)
+```
diff --git a/docs/mpext.md b/docs/mpext.md
new file mode 100644
index 0000000..358fbf9
--- /dev/null
+++ b/docs/mpext.md
@@ -0,0 +1,39 @@
+# Type Extensions
+
+Tarantool supports natively Decimal, uuid and Datetime types. `asynctnt` also supports
+encoding/decoding of such types to Python native `Decimal`, `UUID` and `datetime` types respectively.
+
+Some examples:
+
+```lua
+local s = box.schema.create_space('wallets')
+s:format({
+    { type = 'unsigned', name = 'id' },
+    { type = 'uuid', name = 'uuid' },
+    { type = 'decimal', name = 'money' },
+    { type = 'datetime', name = 'created_at' },
+})
+s:create_index('primary')
+```
+
+And some python usage:
+
+```python
+import pytz
+import datetime
+import uuid
+import asynctnt
+
+from decimal import Decimal
+
+Moscow = pytz.timezone('Europe/Moscow')
+
+conn = await asynctnt.connect()
+
+await conn.insert('wallets', {
+    'id': 1,
+    'uuid': uuid.uuid4(),
+    'money': Decimal('42.17'),
+    'created_at': datetime.datetime.now(tz=Moscow)
+})
+```
diff --git a/docs/overview.md b/docs/overview.md
new file mode 120000
index 0000000..32d46ee
--- /dev/null
+++ b/docs/overview.md
@@ -0,0 +1,1 @@
+../README.md
\ No newline at end of file
diff --git a/docs/pushes.md b/docs/pushes.md
new file mode 100644
index 0000000..331aab7
--- /dev/null
+++ b/docs/pushes.md
@@ -0,0 +1,71 @@
+# Session Push
+
+Tarantool 1.10 introduced session pushes which gives an ability to receive
+out of bound notifications from Tarantool server
+
+For example let's consider this simple example:
+
+```lua
+function sub(n)
+    for i=1,n do
+        box.session.push(i, i * i)
+    end
+
+    return 'done'
+end
+```
+
+
+This function will yield `n` push messages to the client before returning.
+To receive such notification in Python using `asynctnt` we need to subscribe
+first for these notifications and then use `PushIterator` to iterate over
+all the messages from Tarantool:
+
+```python
+import asyncio
+import asynctnt
+
+
+async def main():
+    async with asynctnt.Connection(port=3301) as conn:
+        fut = conn.call('sub', [10], push_subscribe=True)
+        it = asynctnt.PushIterator(fut)
+
+        async for value in it:
+            print(value)
+
+asyncio.run(main())
+```
+
+This will produce the following output:
+
+```bash
+$ python example.py
+[1, 1]
+[2, 4]
+[3, 9]
+[4, 16]
+[5, 25]
+```
+
+
+In order to receive a return value you can simply `await` on the initially
+returned future from the `call()` method:
+
+```python
+import asyncio
+import asynctnt
+
+
+async def main():
+    async with asynctnt.Connection(port=3301) as conn:
+        fut = conn.call('sub', [10], push_subscribe=True)
+        it = asynctnt.PushIterator(fut)
+
+        async for value in it:
+            print(value)
+
+        print(await fut)  # receive the response
+
+asyncio.run(main())
+```
diff --git a/docs/pushes.rst b/docs/pushes.rst
deleted file mode 100644
index 95d8205..0000000
--- a/docs/pushes.rst
+++ /dev/null
@@ -1,75 +0,0 @@
-.. _asynctnt-pushes:
-
-Session Push
-============
-
-Tarantool 1.10 introduced session pushes which gives an ability to receive
-out of bound notifications from Tarantool server
-
-For example let's consider this simple example:
-
-.. code:: lua
-
-    function sub(n)
-        for i=1,n do
-            box.session.push(i, i * i)
-        end
-
-        return 'done'
-    end
-
-
-This function will yield `n` push messages to the client before returning.
-To receive such notification in Python using `asynctnt` we need to subscribe
-first for these notifications and then use `PushIterator` to iterate over
-all the messages from Tarantool:
-
-.. code:: python
-
-    import asyncio
-    import asynctnt
-
-
-    async def main():
-        async with asynctnt.Connection(port=3301) as conn:
-            fut = conn.call('sub', [10], push_subscribe=True)
-            it = asynctnt.PushIterator(fut)
-
-            async for value in it:
-                print(value)
-
-    asyncio.run(main())
-
-
-This will produce the following output:
-
-.. code::
-
-    $ python example.py
-    [1, 1]
-    [2, 4]
-    [3, 9]
-    [4, 16]
-    [5, 25]
-
-
-In order to receive a return value you can simply `await` on the initially
-returned future from the `call()` method:
-
-.. code:: python
-
-    import asyncio
-    import asynctnt
-
-
-    async def main():
-        async with asynctnt.Connection(port=3301) as conn:
-            fut = conn.call('sub', [10], push_subscribe=True)
-            it = asynctnt.PushIterator(fut)
-
-            async for value in it:
-                print(value)
-
-            print(await fut)  # receive the response
-
-    asyncio.run(main())
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 2ba9448..82b0976 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,3 +1,7 @@
-Sphinx
+Sphinx>=5
 sphinx_rtd_theme
 sphinxcontrib-asyncio
+# sphinx-book-theme
+myst-parser
+sphinx-autodoc-typehints
+sphinx-autoapi
diff --git a/docs/sql.md b/docs/sql.md
new file mode 100644
index 0000000..fe5e306
--- /dev/null
+++ b/docs/sql.md
@@ -0,0 +1,122 @@
+# SQL Support
+
+Tarantool 2.x supports SQL interface to the database. `asynctnt` fully supports it including prepared statements and metadata parsing.
+
+## Basic usage
+
+Tarantool config:
+
+```lua
+box.cfg {
+    listen = '127.0.0.1:3301'
+}
+
+box.once('v1', function()
+    box.schema.user.grant('guest', 'read,write,execute', 'universe')
+
+    box.execute([[
+        create table users (
+            id int primary key autoincrement,
+            name text
+        )
+    ]])
+end)
+```
+
+
+Python code:
+
+```python
+import asyncio
+import asynctnt
+
+
+async def main():
+    conn = asynctnt.Connection(host='127.0.0.1', port=3301)
+    await conn.connect()
+
+    await conn.execute("insert into users (name) values (?)", ['James Bond'])
+    resp = await conn.execute("insert into users (name) values (:name)", [{':name', 'Ethan Hunt'}])
+
+    # get value of auto incremented primary key
+    print(resp.autoincrement_ids)
+
+    data = await conn.execute('select * from users')
+
+    for row in data:
+        print(row)
+
+    await conn.disconnect()
+
+asyncio.run(main())
+```
+
+
+Stdout:
+```
+[2]
+<TarantoolTuple ID=1 NAME='James Bond'>
+<TarantoolTuple ID=2 NAME='Ethan Hunt'>
+```
+
+
+## Metadata
+
+You can access all the metadata associated with the SQL response, like so:
+
+```python
+res = await conn.execute("select * from users")
+assert res.metadata.fields[0].name == 'ID'
+assert res.metadata.fields[0].type == 'integer'
+
+assert res.metadata.fields[1].name == 'NAME'
+assert res.metadata.fields[0].type == 'string'
+```
+
+## Prepared statement
+
+You can make use of [prepared statements](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_sql/prepare/)
+in order to execute the same requests more optimally. Simple example:
+
+```python
+stmt = conn.prepare('select id, name from users where id = ?')
+async with stmt:
+    user1 = stmt.execute([1])
+    assert user1.name == 'James Bond'
+
+    user2 = stmt.execute([2])
+    assert user2.name == 'Ethan Hunt'
+```
+
+`prepare()` and `unprepare()` calls are called automatically within a prepared statement context manager.
+You may want to control prepare/unprepare yourself:
+
+```python
+stmt = conn.prepare('select id, name from users where id = ?')
+
+await stmt.prepare()
+
+user1 = stmt.execute([1])
+assert user1.name == 'James Bond'
+
+user2 = stmt.execute([2])
+assert user2.name == 'Ethan Hunt'
+
+await stmt.unprepare()
+```
+
+Named parameters are also supported:
+
+```python
+stmt = conn.prepare('select id, name from users where id = :id')
+async with stmt:
+    user1 = stmt.execute([
+        {':id': 1}
+    ])
+    assert user1.name == 'James Bond'
+
+    user2 = stmt.execute([
+        {':id': 2}
+    ])
+    assert user2.name == 'Ethan Hunt'
+```
diff --git a/docs/streams.md b/docs/streams.md
new file mode 100644
index 0000000..5e49434
--- /dev/null
+++ b/docs/streams.md
@@ -0,0 +1,67 @@
+# Streams and Transactions
+
+## Basic usage
+Interactive transactions are available in Tarantool 2.10+ and are implemented on
+top of streams.
+
+You can easily use streams in `asynctnt`:
+
+```python
+import asynctnt
+
+conn = await asynctnt.connect()
+
+async with conn.stream() as s:
+    data = [1, 'Peter Parker']
+    await s.insert('heroes', data)
+    await s.update('heroes', [1], ['=', 'name', 'Spider-Man'])
+
+res = await conn.select('heroes')
+print(res)
+```
+
+This syntax will call `begin()` and `commit()` methods behind the scenes and a `rollback()`
+method if any exception will happen inside the context manager.
+
+## Isolation
+Everything happening inside in the transaction (a.k.a. stream) is visible only
+to the current stream.
+You may also control the isolation level, but you have to call `begin()` method manually:
+
+```python
+import asynctnt
+from asynctnt.api import Isolation
+
+conn = await asynctnt.connect()
+
+s = conn.stream()
+await s.begin(Isolation.READ_COMMITTED)
+
+data = [1, 'Peter Parker']
+await s.insert('heroes', data)
+await s.update('heroes', [1], ['=', 'name', 'Spider-Man'])
+
+await s.commit()
+
+res = await conn.select('heroes')
+print(res)
+```
+
+## Flexibility
+Tarantool allows to start/end transaction with any way (of course the native functions are the fastest):
+```python
+# begin() variants
+await conn.begin()
+await conn.call('box.begin')
+await conn.execute('START TRANSACTION')
+
+# commit() variants
+await conn.commit()
+await conn.call('box.commit')
+await conn.execute('COMMIT')
+
+# rollback() variants
+await conn.rollback()
+await conn.call('box.rollback')
+await conn.execute('ROLLBACK')
+```
diff --git a/docs/ttuple.md b/docs/ttuple.md
new file mode 100644
index 0000000..f750499
--- /dev/null
+++ b/docs/ttuple.md
@@ -0,0 +1,1 @@
+# Tarantool Tuple
diff --git a/requirements.txt b/requirements.txt
index 31c59b0..506607d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,10 +1,10 @@
 PyYAML>=5.0
 uvloop>=0.12.0
-Cython==0.29.24
-pep8==1.7.1
-flake8==3.7.9
+Cython==0.29.30
+pep8
+flake8
 twine
-Sphinx
-sphinx_rtd_theme
-sphinxcontrib-asyncio
 coverage
+pytz
+python-dateutil
+-r docs/requirements.txt
diff --git a/setup.cfg b/setup.cfg
index b88034e..08aedd7 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,2 @@
 [metadata]
-description-file = README.md
+description_file = README.md
diff --git a/setup.py b/setup.py
index 6084a4a..307219e 100644
--- a/setup.py
+++ b/setup.py
@@ -25,7 +25,7 @@ def find_version():
                 r"""__version__\s*=\s*(['"])([^'"]+)\1""", line).group(2)
 
 
-CYTHON_VERSION = '0.29.24'
+CYTHON_VERSION = '0.29.30'
 
 
 class build_ext(_build_ext.build_ext):
@@ -126,7 +126,6 @@ def finalize_options(self):
     classifiers=[
         "Programming Language :: Python :: 3",
         "Programming Language :: Python :: 3 :: Only",
-        "Programming Language :: Python :: 3.5",
         "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
diff --git a/temp/bench.py b/temp/bench.py
deleted file mode 100644
index 5425e38..0000000
--- a/temp/bench.py
+++ /dev/null
@@ -1,131 +0,0 @@
-import argparse
-import asyncio
-import datetime
-import logging
-import math
-import sys
-
-
-def main():
-    logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
-    parser = argparse.ArgumentParser()
-    parser.add_argument('--asynctnt', type=bool, default=False)
-    parser.add_argument('--aiotnt', type=bool, default=False)
-    parser.add_argument('--tarantool', type=bool, default=False)
-    parser.add_argument('--uvloop', type=bool, default=False)
-    parser.add_argument('-n', type=int, default=50000,
-                        help='number of executed requests')
-    parser.add_argument('-b', type=int, default=100, help='number of bulks')
-    args = parser.parse_args()
-
-    if args.uvloop:
-        import uvloop
-        asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
-
-    loop = asyncio.get_event_loop()
-
-    print('Running \'{}\' for {} requests in {} batches. '
-          'Using uvloop: {}\n'.format(
-        'aiotarantool' if args.aiotnt else 'asynctnt',
-        args.n, args.b, args.uvloop)
-    )
-
-    if args.aiotnt:
-        loop.run_until_complete(
-            bench_aiotarantool(args.n, args.b)
-        )
-    elif args.tarantool:
-        bench_tarantool(args.n, 1)
-    else:
-        loop.run_until_complete(
-            bench_asynctnt(args.n, args.b)
-        )
-
-
-async def bench_asynctnt(n, b):
-    import asynctnt
-    from asynctnt.iproto.protocol import Iterator
-
-    conn = asynctnt.Connection(host='127.0.0.1',
-                               port=3305,
-                               username='t1',
-                               password='t1',
-                               reconnect_timeout=1)
-    await conn.connect()
-
-    n_requests_per_bulk = int(math.ceil(n / b))
-
-    async def bulk_f():
-        for _ in range(n_requests_per_bulk):
-            # await conn.ping()
-            # await conn.call('test')
-            # await conn.eval('return "hello"')
-            await conn.select(512)
-            # await conn.sql('select 1 as a, 2 as b')
-            # await conn.replace('tester', [2, 'hhhh'])
-            # await conn.update('tester', [2], [(':', 1, 1, 3, 'yo!')])
-
-    coros = [bulk_f() for _ in range(b)]
-
-    start = datetime.datetime.now()
-    await asyncio.wait(coros)
-    end = datetime.datetime.now()
-
-    elapsed = end - start
-    print('Elapsed: {}, RPS: {}. TPR: {}'.format(elapsed, n / elapsed.total_seconds(), elapsed.total_seconds() / n))
-
-
-async def bench_aiotarantool(n, b):
-    import aiotarantool
-
-    conn = aiotarantool.connect('127.0.0.1', 3305,
-                                user='t1', password='t1')
-
-    n_requests_per_bulk = int(math.ceil(n / b))
-
-    async def bulk_f():
-        for _ in range(n_requests_per_bulk):
-            await conn.ping()
-            # await conn.call('test')
-            # await conn.eval('return "hello"')
-            # await conn.select(512)
-            # await conn.replace('tester', [2, 'hhhh'])
-            # await conn.update('tester', [2], [(':', 1, 1, 3, 'yo!')])
-
-    coros = [bulk_f() for _ in range(b)]
-
-    start = datetime.datetime.now()
-    await asyncio.wait(coros)
-    end = datetime.datetime.now()
-
-    elapsed = end - start
-    print('Elapsed: {}, RPS: {}'.format(elapsed, n / elapsed.total_seconds()))
-
-
-def bench_tarantool(n, b):
-    import tarantool
-
-    conn = tarantool.Connection(host='127.0.0.1',
-                                port=3305,
-                                user='t1',
-                                password='t1')
-    conn.connect()
-    b = 1
-    n_requests_per_bulk = int(math.ceil(n / b))
-
-    start = datetime.datetime.now()
-    for _ in range(n_requests_per_bulk):
-        # conn.ping()
-        # await conn.call('test')
-        # await conn.eval('return "hello"')
-        conn.select(512)
-        # await conn.replace('tester', [2, 'hhhh'])
-        # await conn.update('tester', [2], [(':', 1, 1, 3, 'yo!')])
-
-    end = datetime.datetime.now()
-    elapsed = end - start
-    print('Elapsed: {}, RPS: {}'.format(elapsed, n / elapsed.total_seconds()))
-
-
-if __name__ == '__main__':
-    main()
diff --git a/temp/bench_cmp.py b/temp/bench_cmp.py
deleted file mode 100644
index bbdd2ab..0000000
--- a/temp/bench_cmp.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import asyncio
-import uvloop; asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
-import datetime
-
-
-def create_tuple(i, size):
-    s = 'x' * size
-    return []
-
-async def run_asynctnt():
-    import asynctnt
-    conn = asynctnt.Connection(host='127.0.0.1', port=3303)
-    await conn.connect()
-
-    n = 100000
-    size = 1.5 * 1024
-
-    start = datetime.datetime.now()
-
-    for i in range(1, n + 1):
-        # await conn.insert('tester', [i, 'x' * int(size)])
-        # await conn.select('tester', [i])
-        await conn.delete('tester', [i])
-
-    # values = await conn.select('tester', [])
-
-    end = datetime.datetime.now()
-    elapsed = end - start
-    print('Elapsed: {}, RPS: {}'.format(elapsed, n / elapsed.total_seconds()))
-
-    await conn.disconnect()
-
-
-loop = asyncio.get_event_loop()
-loop.run_until_complete(run_asynctnt())
diff --git a/temp/bug_push_hangs.py b/temp/bug_push_hangs.py
deleted file mode 100644
index 8f6ffae..0000000
--- a/temp/bug_push_hangs.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import asyncio
-import asynctnt
-
-
-def get_push_iterator(connection):
-    fut = connection.call("infinite_push_loop", push_subscribe=True)
-    return fut, asynctnt.PushIterator(fut)
-
-
-async def main():
-    async with asynctnt.Connection(port=3301) as conn:
-
-        fut, it = get_push_iterator(conn)
-        transport_id = id(conn._transport)
-
-        while True:
-            current_transport_id = id(conn._transport)
-            if current_transport_id != transport_id:
-                transport_id = current_transport_id
-                fut, it = get_push_iterator(conn)
-
-            try:
-                result = await it.__anext__()
-                # result = await asyncio.wait_for(it.__anext__(), timeout=10)
-                print(result)
-            except asyncio.TimeoutError:
-                print('timeout')
-                pass
-            except Exception as e:
-                # res = await fut
-                # print(res)
-                print(e)
-                return
-
-
-if __name__ == "__main__":
-    asyncio.run(main())
diff --git a/temp/demo.lua b/temp/demo.lua
deleted file mode 100644
index 4df8f16..0000000
--- a/temp/demo.lua
+++ /dev/null
@@ -1,32 +0,0 @@
-box.cfg {
-    listen = '127.0.0.1:3301',
-    wal_mode = 'none'
-}
-
-box.once('v1', function()
-    box.schema.user.grant('guest', 'read,write,execute', 'universe')
-
-    local s = box.schema.create_space('tester')
-    s:create_index('primary')
-    s:format({
-        {name='id', type='unsigned'},
-        {name='text', type='string'},
-    })
-end)
-
-
-function f(...)
-    return ...
-end
-
-
-local x = 0
-
-function infinite_push_loop()
-    local fiber = require('fiber')
-    while true do
-        fiber.sleep(2)
-        x = x + 1
-        box.session.push(x)
-    end
-end
diff --git a/temp/demo/demo.py b/temp/demo/demo.py
deleted file mode 100644
index 4515d6f..0000000
--- a/temp/demo/demo.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# import asyncio
-#
-# import asynctnt
-#
-#
-# async def main():
-#     conn = asynctnt.Connection(host='127.0.0.1', port=3301)
-#     await conn.connect()
-#
-#     for i in range(1, 11):
-#         await conn.replace('tester', [i, 'hello{}'.format(i)])
-#
-#     data = await conn.select('tester', [])
-#     first_tuple = data[0]
-#     print('tuple:', first_tuple)
-#     print(f'tuple[0]: {first_tuple[0]}; tuple["id"]: {first_tuple["id"]}')
-#     print(f'tuple[1]: {first_tuple[1]}; tuple["name"]: {first_tuple["name"]}')
-#
-#     await conn.disconnect()
-#
-#
-# asyncio.run(main())
-
-
-import asyncio
-from pprint import pprint
-
-import asynctnt
-import logging
-
-# logging.basicConfig(level=logging.DEBUG)
-
-async def main():
-    conn = asynctnt.Connection(host='127.0.0.1', port=3301)
-    await conn.connect()
-
-    # await conn.sql("""
-    #     create table users (
-    #         id int primary key,
-    #         name text
-    #     )
-    # """)
-    # await conn.sql("insert into users (id, name) values (1, 'James Bond')")
-    # await conn.sql("insert into users (id, name) values (2, 'Ethan Hunt')")
-    # data = await conn.sql('select * from users')
-    #
-    # for row in data:
-    #     print(row)
-    #
-    # await conn.disconnect()
-
-    print(await conn.sql("select * from users", parse_metadata=False))
-
-
-asyncio.run(main())
\ No newline at end of file
diff --git a/temp/demo/init.lua b/temp/demo/init.lua
deleted file mode 100644
index c1ac5a2..0000000
--- a/temp/demo/init.lua
+++ /dev/null
@@ -1,27 +0,0 @@
-box.cfg {
-    listen = '0.0.0.0:3301'
-}
-
-box.once('v1', function()
-    pcall(box.schema.user.grant, 'guest', 'read,write,execute', 'universe')
-
-    local s = box.schema.create_space('tester')
-    s:create_index('primary')
-    s:format({
-        {name='id', type='unsigned'},
-        {name='name', type='string'},
-    })
-end)
-
-box.once('v2', function()
-    pcall(box.schema.user.grant, 'guest', 'read,write,execute', 'universe')
-
-    box.execute([[
-        create table users (
-            id int primary key,
-            name text
-        )
-    ]])
-end)
-
-require 'console'.start()
diff --git a/temp/instance.py b/temp/instance.py
deleted file mode 100644
index d65e74c..0000000
--- a/temp/instance.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import asyncio
-# import uvloop; asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
-
-import logging
-
-import sys
-
-from asynctnt.instance import TarantoolAsyncInstance, \
-    TarantoolSyncInstance
-
-logging.basicConfig(level=logging.DEBUG)
-
-t = TarantoolSyncInstance(
-    host='unix/',
-    port='/tmp/_mytnt.sock',
-    console_host='127.0.0.1',
-    applua=open('../tests/files/app.lua').read())
-
-t.start()
-t.stop()
-
-
-sys.exit(0)
-
-
-async def main(t, loop):
-    await t.start()
-    data = await t.command("box.info.status")
-    print(data)
-    await t.stop()
-
-    await t.start()
-    data = await t.command("box.info.status")
-    print(data)
-    await t.stop()
-    # await t.wait_stopped()
-
-event_loop = asyncio.get_event_loop()
-asyncio.get_child_watcher().attach_loop(event_loop)
-
-t = TarantoolAsyncInstance(
-    host='unix/',
-    port='/tmp/_mytnt.sock',
-    console_host='127.0.0.1',
-    applua=open('../tests/files/app.lua').read())
-try:
-    event_loop.run_until_complete(main(t, event_loop))
-    # event_loop.run_until_complete(t.start())
-    # event_loop.run_until_complete(t.wait_stopped())
-except KeyboardInterrupt:
-    event_loop.run_until_complete(t.stop())
-finally:
-    event_loop.close()
diff --git a/temp/memleak.py b/temp/memleak.py
deleted file mode 100644
index 50b9474..0000000
--- a/temp/memleak.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import asyncio
-import logging
-import sys
-
-import asynctnt
-
-logging.basicConfig(level=logging.DEBUG)
-
-
-async def main():
-    c = asynctnt.Connection(
-        host='localhost',
-        port=3305,
-        connect_timeout=5,
-        request_timeout=5,
-        reconnect_timeout=1/3,
-    )
-    async with c:
-        while True:
-            res = await c.eval('local t ={}; for i=1,1000000 do t[i] = {i + 0.03} end; return t')
-            print(sys.getrefcount(res.body[0][-1]))
-
-
-asyncio.run(main())
diff --git a/temp/push.py b/temp/push.py
deleted file mode 100644
index 81c3870..0000000
--- a/temp/push.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import asyncio
-import time
-
-import asynctnt
-import logging
-
-logging.basicConfig(level=logging.DEBUG)
-
-
-async def run():
-    conn = asynctnt.Connection(host='127.0.0.1', port=3305)
-    await conn.connect()
-
-    fut = conn.call("asyncaction", push_subscribe=True)
-    async for row in asynctnt.PushIterator(fut):
-        print('wow', row)
-
-    print(await fut)
-    # await asyncio.sleep(20)
-
-loop = asyncio.get_event_loop()
-loop.run_until_complete(run())
diff --git a/temp/resp.py b/temp/resp.py
deleted file mode 100644
index fa0bd28..0000000
--- a/temp/resp.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import asynctnt
-import asyncio
-
-
-async def f():
-    c = await asynctnt.connect()
-    c.select(281)
-
-
-loop = asyncio.get_event_loop()
-res = loop.run_until_complete(f())
-
-t = res[0]
-print(type(res), len(res))
-print(type(t), len(t))
-
-print(t)
-print(list(t))
-for k, v in t.items():
-    print(k , v)
diff --git a/temp/schema_fail.py b/temp/schema_fail.py
deleted file mode 100644
index 8d2e3f7..0000000
--- a/temp/schema_fail.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import asyncio
-import asynctnt
-
-import logging
-
-logging.basicConfig(level=logging.DEBUG)
-
-
-async def main():
-    conn = asynctnt.Connection(host='127.0.0.1', port=3301, username="storage", password="passw0rd")
-    await conn.connect()
-    logging.info('connected')
-
-    await conn.call('box.schema.space.create', ['geo', {"if_not_exists": True}])
-    logging.info('space_create')
-    await conn.call('box.space.geo:format', [[{"name": "id", "type": "string"}]])
-    logging.info('space_format')
-
-    await conn.disconnect()
-
-
-asyncio.run(main())
diff --git a/temp/sql.py b/temp/sql.py
deleted file mode 100644
index 6f9fd18..0000000
--- a/temp/sql.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import asyncio
-import time
-
-import asynctnt
-import logging
-
-logging.basicConfig(level=logging.DEBUG)
-
-
-async def run():
-    conn = asynctnt.Connection(host='127.0.0.1', port=3305)
-    await conn.connect()
-
-    resp = await conn.sql("insert into s (id, name) values (1, 'one')")
-    # resp = await conn.sql("select * from s")
-    print(resp)
-    print(resp.rowcount)
-
-loop = asyncio.get_event_loop()
-loop.run_until_complete(run())
diff --git a/temp/time_connect.py b/temp/time_connect.py
deleted file mode 100644
index ba77c99..0000000
--- a/temp/time_connect.py
+++ /dev/null
@@ -1,67 +0,0 @@
-import asyncio
-import time
-
-import asynctnt
-import logging
-
-logging.basicConfig(level=logging.DEBUG)
-
-
-async def run():
-    conn = asynctnt.Connection(host='127.0.0.1', port=3305)
-    begin = time.time()
-    n = 1000
-    for i in range(n):
-        await conn.connect()
-        await conn.disconnect()
-
-    dt = time.time() - begin
-    print('Total: {}'.format(dt))
-    print('1 connect+disconnect: {}'.format(dt / n))
-
-
-async def run2():
-    conn = asynctnt.Connection(host='127.0.0.1', port=3305)
-    await conn.connect()
-    begin = time.time()
-    n = 10000
-    for i in range(n):
-        await conn.eval('return box.info')
-
-    dt = time.time() - begin
-    print('Total: {}'.format(dt))
-    print('1 ping: {}'.format(dt / n))
-
-
-async def run3():
-    conn = asynctnt.Connection(host='127.0.0.1', port=3301)
-    await conn.connect()
-
-    begin = time.time()
-    n = 10000
-    for i in range(n):
-        await conn.select('S')
-    dt = time.time() - begin
-    print('Total: {}'.format(dt))
-    print('1 select: {}'.format(dt / n))
-
-
-async def run4():
-    conn = asynctnt.Connection(host='127.0.0.1', port=3301)
-    await conn.connect()
-
-    begin = time.time()
-    n = 10000
-    for i in range(n):
-        resp = await conn.sql('select * from s')
-    dt = time.time() - begin
-    print('Total: {}'.format(dt))
-    print('1 sql: {}'.format(dt / n))
-
-    print(resp.encoding)
-    print(resp)
-    print(list(resp))
-
-loop = asyncio.get_event_loop()
-loop.run_until_complete(run3())
-loop.run_until_complete(run4())
diff --git a/temp/with_crash.py b/temp/with_crash.py
deleted file mode 100644
index d45fa5a..0000000
--- a/temp/with_crash.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import asynctnt
-import asyncio
-import logging
-from asynctnt.exceptions import TarantoolNotConnectedError
-
-logging.basicConfig(level=logging.DEBUG)
-
-
-async def main():
-    c = asynctnt.Connection(
-        host='localhost',
-        port=3305,
-        connect_timeout=5,
-        request_timeout=5,
-        reconnect_timeout=1/3,
-    )
-    try:
-        while True:
-            if not c.is_connected:
-                print('started connecting')
-                await c.connect()  # <------------- Hangs here after the Tarantool instance crashes
-                print('connected')
-            try:
-                input('press any key to segfalt...')
-                await c.eval('''require('ffi').cast('char *', 0)[0] = 48''')
-            except TarantoolNotConnectedError as e:
-                print('EXCEPTION:', e.__class__.__name__, e)
-    finally:
-        await c.disconnect()
-        print('disconnected')
-
-
-asyncio.run(main())
\ No newline at end of file
diff --git a/third_party/msgpuck b/third_party/msgpuck
index 8ae606a..0c6680a 160000
--- a/third_party/msgpuck
+++ b/third_party/msgpuck
@@ -1,1 +1,1 @@
-Subproject commit 8ae606a1636dd89b2d61b154e5a1db03dce91657
+Subproject commit 0c6680a300e31714f475a7f90c2d95a02d001d80
diff --git a/third_party/xd.h b/third_party/xd.h
new file mode 100644
index 0000000..2f21586
--- /dev/null
+++ b/third_party/xd.h
@@ -0,0 +1,115 @@
+#ifndef XD_H__
+#define XD_H__
+
+#include <stdint.h>
+#include <stdio.h>
+
+#define HEX_SZ (16*10 + 1)
+#define CHR_SZ (16*8 + 1)
+
+typedef struct {
+	uint8_t row;
+	uint8_t hpad;
+	uint8_t cpad;
+	uint8_t hsp;
+	uint8_t csp;
+	uint8_t cols;
+} xd_conf;
+
+static xd_conf default_xd_conf = { 16,1,0,1,1,4 };
+
+static char * xd_extra(char *data, size_t size, xd_conf *cf)
+{
+	/* dumps size bytes of *data to stdout. Looks like:
+	 * [0000] 75 6E 6B 6E 6F 77 6E 20 30 FF 00 00 00 00 39 00 unknown 0.....9.
+	 * src = 16 bytes.
+	 * dst = 6       +  16 * 3   +      4*2         +  16       + 1
+	 *       prefix    byte+pad    sp between col    visual     newline
+	 */
+	if (!cf) cf = &default_xd_conf;
+	uint8_t row  = cf->row;
+	uint8_t hpad = cf->hpad;
+	uint8_t cpad = cf->cpad;
+	uint8_t hsp  = cf->hsp;
+	uint8_t csp  = cf->csp;
+	uint8_t sp   = cf->cols;
+
+	uint8_t every = (uint8_t)row / sp;
+
+	char *p = data;
+	unsigned char c;
+	size_t n;
+	// unsigned addr;
+	// char bytestr[4] = {0};
+	char addrstr[10] = {0};
+	char hexstr[ HEX_SZ ] = {0};
+	char chrstr[ CHR_SZ ] = {0};
+	unsigned hex_sz = row*(2+hpad) + hsp * sp + 1; /* size = bytes<16*2> + 16*<hpad> + col<hsp*sp> */
+	unsigned chr_sz = row*(2+cpad) + csp * sp + 1; /* size = bytes<16> + 16*cpad + col<csp*sp> */
+
+	if ( hex_sz > HEX_SZ ) {
+		fprintf(stderr,"Parameters too big: estimated hex size will be %u, but have only %u\n", hex_sz, HEX_SZ);
+		return NULL;
+	}
+	if ( chr_sz > CHR_SZ ) {
+		fprintf(stderr,"Parameters too big: estimated chr size will be %u, but have only %u\n", chr_sz, CHR_SZ);
+		return NULL;
+	}
+
+	size_t sv_sz = ( size + row-1 ) * ( (uint8_t)( 6 + 3 + hex_sz + 2 + chr_sz + 1 + row-1 ) / row );
+	/*                      ^ reserve for incomplete string             \n      ^ emulation of ceil */
+	char *rv = malloc(sv_sz);
+	if (!rv) {
+		fprintf(stderr,"Can't allocate memory\n");
+		return NULL;
+	}
+	char *rvptr = rv;
+
+	char *curhex = hexstr;
+	char *curchr = chrstr;
+	for(n=1; n<=size; n++) {
+		if (n % row == 1)
+			snprintf(addrstr, sizeof(addrstr), "%04x", ( (int)(p-data) ) & 0xffff );
+
+		c = *p;
+		if (c < 0x20 || c > 0x7f) {
+			c = '.';
+		}
+
+		/* store hex str (for left side) */
+		snprintf(curhex, 3+hpad, "%02X%-*s", (unsigned char)*p, hpad,""); curhex += 2+hpad;
+
+		/* store char str (for right side) */
+		snprintf(curchr, 2+cpad, "%c%-*s", c, cpad, ""); curchr += 1+cpad;
+
+		//warn("n=%d, row=%d, every=%d\n",n,row,every);
+		if( n % row == 0 ) {
+			/* line completed */
+			//printf("[%-4.4s]   %s  %s\n", addrstr, hexstr, chrstr);
+			rvptr += snprintf(rvptr, (p-rvptr+sv_sz) ,"[%-4.4s]   %s  %s\n", addrstr, hexstr, chrstr);
+			//sv_catpvf(rv,"[%-4.4s]   %-*s %-*s\n", addrstr, hex_sz-1, hexstr, chr_sz-1, chrstr);
+			hexstr[0] = 0; curhex = hexstr;
+			chrstr[0] = 0; curchr = chrstr;
+		} else if( every && ( n % every == 0 ) ) {
+			/* half line: add whitespaces */
+			snprintf(curhex, 1+hsp, "%-*s", hsp, ""); curhex += hsp;
+			snprintf(curchr, 1+csp, "%-*s", csp, ""); curchr += csp;
+		}
+		p++; /* next byte */
+	}
+
+	if (curhex > hexstr) {
+		/* print rest of buffer if not empty */
+		//printf("[%4.4s]   %s  %s\n", addrstr, hexstr, chrstr);
+		rvptr += snprintf(rvptr, (p-rvptr+sv_sz),"[%-4.4s]   %-*s %-*s\n", addrstr, hex_sz-1, hexstr, chr_sz-1, chrstr);
+	}
+	//warn("String len: %d, sv_sz=%d",SvCUR(rv),sv_sz);
+	return rv;
+}
+
+static char * xd(char *data, size_t size)
+{
+    return xd_extra(data, size, NULL);
+}
+
+#endif


Test patch:
diff --git a/asynctnt/_testbase.py b/asynctnt/_testbase.py
index 49e4f43..ec46ad3 100644
--- a/asynctnt/_testbase.py
+++ b/asynctnt/_testbase.py
@@ -8,14 +8,17 @@
 import unittest
 
 import sys
+from typing import Tuple, Optional
 
 import asynctnt
 from asynctnt.instance import \
     TarantoolSyncInstance, TarantoolSyncDockerInstance
 from asynctnt import TarantoolTuple
+from asynctnt.utils import get_running_loop
 
 __all__ = (
-    'TestCase', 'TarantoolTestCase', 'ensure_version', 'check_version'
+    'TestCase', 'TarantoolTestCase', 'ensure_version', 'check_version',
+    'ensure_bin_version',
 )
 
 
@@ -62,27 +65,47 @@ def __new__(mcls, name, bases, ns):
             @functools.wraps(meth)
             def wrapper(self, *args, __meth__=meth, **kwargs):
                 self.loop.run_until_complete(__meth__(self, *args, **kwargs))
+
             ns[methname] = wrapper
 
         return super().__new__(mcls, name, bases, ns)
 
 
 class TestCase(unittest.TestCase, metaclass=TestCaseMeta):
-    loop = None
+    _loop = None
+    _use_uvloop = False
 
     @classmethod
     def setUpClass(cls):
-        use_uvloop = os.environ.get('USE_UVLOOP')
+        cls._use_uvloop = os.environ.get('USE_UVLOOP')
 
-        if use_uvloop:
+        if cls._use_uvloop:
             import uvloop
             uvloop.install()
 
-        cls.loop = asyncio.get_event_loop()
+        try:
+            loop = get_running_loop()
+            # if there is a running loop - close it
+            loop.close()
+        except RuntimeError:
+            pass
+
+        cls._loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(cls._loop)
 
-        if use_uvloop:
+        if cls._use_uvloop:
             import uvloop
-            assert isinstance(cls.loop, uvloop.Loop)
+            assert isinstance(cls._loop, uvloop.Loop)
+
+    @classmethod
+    def tearDownClass(cls):
+        if cls._loop is not None:
+            cls._loop.close()
+            cls._loop = None
+
+    @property
+    def loop(self):
+        return self._loop
 
     @contextlib.contextmanager
     def assertRunUnder(self, delta):
@@ -109,6 +132,7 @@ class TarantoolTestCase(TestCase):
     LOGGING_STREAM = sys.stderr
     TNT_APP_LUA_PATH = None
     TNT_CLEANUP = True
+    EXTRA_BOX_CFG = ""
 
     tnt = None
     in_docker = False
@@ -119,15 +143,27 @@ def read_applua(cls):
             with open(cls.TNT_APP_LUA_PATH, 'r') as f:
                 return f.read()
 
+    @classmethod
+    def init_instance(cls):
+        tnt, in_docker = cls._make_instance(
+            extra_box_cfg=cls.EXTRA_BOX_CFG
+        )
+        cls.tnt = tnt
+        cls.in_docker = in_docker
+
     @classmethod
     def setUpClass(cls):
         TestCase.setUpClass()
         logging.basicConfig(level=cls.LOGGING_LEVEL,
                             stream=cls.LOGGING_STREAM)
-        tnt, in_docker = cls._make_instance()
-        tnt.start()
-        cls.tnt = tnt
-        cls.in_docker = in_docker
+        if cls.tnt is None:
+            tnt, in_docker = cls._make_instance(
+                extra_box_cfg=cls.EXTRA_BOX_CFG
+            )
+            cls.tnt = tnt
+            cls.in_docker = in_docker
+
+        cls.tnt.start()
 
     @classmethod
     def make_instance(cls):
@@ -135,7 +171,7 @@ def make_instance(cls):
         return obj
 
     @classmethod
-    def _make_instance(cls, *args, **kwargs):
+    def _make_instance(cls, **kwargs):
         tarantool_docker_image = os.getenv('TARANTOOL_DOCKER_IMAGE')
         tarantool_docker_tag = os.getenv('TARANTOOL_DOCKER_VERSION')
         in_docker = False
@@ -147,7 +183,9 @@ def _make_instance(cls, *args, **kwargs):
             tnt = TarantoolSyncDockerInstance(
                 applua=cls.read_applua(),
                 docker_image=tarantool_docker_image,
-                docker_tag=tarantool_docker_tag
+                docker_tag=tarantool_docker_tag,
+                timeout=4 * 60,
+                **kwargs
             )
             in_docker = True
         else:
@@ -157,7 +195,8 @@ def _make_instance(cls, *args, **kwargs):
                     port=TarantoolSyncInstance.get_random_port(),
                     console_port=TarantoolSyncInstance.get_random_port(),
                     applua=cls.read_applua(),
-                    cleanup=cls.TNT_CLEANUP
+                    cleanup=cls.TNT_CLEANUP,
+                    **kwargs
                 )
             else:
                 tnt = TarantoolSyncInstance(
@@ -165,8 +204,10 @@ def _make_instance(cls, *args, **kwargs):
                     port=unix_path,
                     console_host='127.0.0.1',
                     applua=cls.read_applua(),
-                    cleanup=cls.TNT_CLEANUP
+                    cleanup=cls.TNT_CLEANUP,
+                    **kwargs
                 )
+
         return tnt, in_docker
 
     @classmethod
@@ -192,7 +233,7 @@ async def tnt_connect(self, *,
                           username=None, password=None,
                           fetch_schema=True,
                           auto_refetch_schema=False,
-                          connect_timeout=None, reconnect_timeout=1/3,
+                          connect_timeout=None, reconnect_timeout=1 / 3,
                           ping_timeout=0,
                           request_timeout=None, encoding='utf-8',
                           initial_read_buffer_size=None):
@@ -239,7 +280,7 @@ def assertResponseEqualKV(self, resp, target, *args):
 
 
 def ensure_version(*, min=None, max=None,
-                   min_included=False, max_included=False):
+                   min_included=True, max_included=False):
     def check_version_wrap(f):
         @functools.wraps(f)
         async def wrap(self, *args, **kwargs):
@@ -251,22 +292,50 @@ async def wrap(self, *args, **kwargs):
                 if inspect.isawaitable(res):
                     return await res
                 return res
+
         return wrap
+
+    return check_version_wrap
+
+
+def ensure_bin_version(*, min=None, max=None,
+                       min_included=True, max_included=False):
+    def check_version_wrap(cls):
+        assert issubclass(cls, TarantoolTestCase)
+        cls.init_instance()
+        ok, reason = _check_version(cls.tnt.bin_version,
+                                    min=min, max=max,
+                                    min_included=min_included,
+                                    max_included=max_included)
+
+        if not ok:
+            cls = unittest.skip(reason)(cls)
+
+        return cls
+
     return check_version_wrap
 
 
 def check_version(test, version, *, min=None, max=None,
                   min_included=False, max_included=False):
-    if min and (version < min or (min_included and version <= min)):
-        test.skipTest(
-            'version mismatch - required min={} got={}'.format(
-                min, version))
-        return False
+    ok, reason = _check_version(version, min=min, max=max,
+                                min_included=min_included,
+                                max_included=max_included)
 
-    if max and (version > max or (max_included and version >= max)):
-        test.skipTest(
-            'version mismatch - required max={} got={}'.format(
-                max, version))
+    if not ok:
+        test.skipTest(reason)
         return False
 
     return True
+
+
+def _check_version(version, *, min=None, max=None,
+                   min_included=False,
+                   max_included=False) -> Tuple[bool, Optional[str]]:
+    if min and (version < min or (min_included and version <= min)):
+        return False, f'version mismatch - required min={min} got={version}'
+
+    if max and (version > max or (max_included and version >= max)):
+        return False, f'version mismatch - required max={max} got={version}'
+
+    return True, None
diff --git a/tests/files/app.lua b/tests/files/app.lua
index 2388181..ff2fb83 100644
--- a/tests/files/app.lua
+++ b/tests/files/app.lua
@@ -65,6 +65,9 @@ local function bootstrap()
         b.types.unsigned = 'num'
         b.types.integer = 'int'
     end
+    b.types.decimal = 'decimal'
+    b.types.uuid = 'uuid'
+    b.types.datetime = 'datetime'
     b.types.number = 'number'
     b.types.array = 'array'
     b.types.scalar = 'scalar'
@@ -93,6 +96,7 @@ box.once('v1', function()
 
     if B:check_version({2, 0}) then
         box.schema.user.grant('t1', 'read,write,execute,create,drop,alter', 'universe')
+        box.schema.user.grant('guest', 'read,write,execute,create,drop,alter', 'universe')
     else
         box.schema.user.grant('t1', 'read,write,execute', 'universe')
     end
@@ -119,7 +123,7 @@ if B:check_version({2, 0}) then
         box.execute([[
             CREATE TABLE sql_space (
                 id INT PRIMARY KEY,
-                name TEXT
+                name TEXT COLLATE "unicode"
             )
         ]])
         box.execute([[
@@ -135,6 +139,35 @@ if B:check_version({2, 0}) then
             )
         ]])
     end)
+
+    box.once('v2.1', function()
+        if B:check_version({2, 2}) then
+            local s = box.schema.create_space('tester_ext_dec')
+            s:format({
+                {type=B.types.unsigned, name='f1'},
+                {type=B.types.decimal, name='f2'},
+            })
+            s:create_index('primary')
+        end
+
+        if B:check_version({2, 4, 1}) then
+            s = box.schema.create_space('tester_ext_uuid')
+            s:format({
+                {type=B.types.unsigned, name='f1'},
+                {type=B.types.uuid, name='f2'},
+            })
+            s:create_index('primary')
+        end
+
+        if B:check_version({2, 10, 0}) then
+            s = box.schema.create_space('tester_ext_datetime')
+            s:format({
+                {type=B.types.unsigned, name='id'},
+                {type=B.types.datetime, name='dt'},
+            })
+            s:create_index('primary')
+        end
+    end)
 end
 
 
@@ -144,24 +177,25 @@ function make_third_index(name)
 end
 
 
-function truncate()
+local function _truncate(sp)
+    if sp == nil then
+        return
+    end
+
     local keys = {}
-    for _, el in box.space.tester:pairs() do
+    for _, el in sp:pairs() do
         table.insert(keys, el[1])
     end
 
     for _, k in ipairs(keys) do
-        box.space.tester:delete({k})
+        sp:delete({k})
     end
+end
 
-    keys = {}
-    for _, el in box.space.no_schema_space:pairs() do
-        table.insert(keys, el[1])
-    end
 
-    for _, k in ipairs(keys) do
-        box.space.no_schema_space:delete({k})
-    end
+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')
@@ -174,6 +208,10 @@ function truncate()
     if box.space.SQL_SPACE_AUTOINCREMENT_MULTIPLE ~= nil then
         box.execute('DELETE FROM sql_space_autoincrement_multiple')
     end
+
+    _truncate(box.space.tester_ext_dec)
+    _truncate(box.space.tester_ext_uuid)
+    _truncate(box.space.tester_ext_datetime)
 end
 
 
diff --git a/tests/test_common.py b/tests/test_common.py
index 39157c6..8a4fcd9 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -1,7 +1,6 @@
 import asyncio
 
 import asynctnt
-from asynctnt._testbase import ensure_version
 from asynctnt.exceptions import TarantoolNotConnectedError, \
     TarantoolDatabaseError
 from tests import BaseTarantoolTestCase
@@ -44,20 +43,26 @@ async def test__schema_refetch_on_schema_change(self):
 
         # Changing scheme
         await self.conn.eval(
-            "local s = box.schema.create_space('new_space');"
-            "s:drop();"
+            "box.schema.create_space('new_space');"
         )
 
         try:
-            await self.conn.ping()
-        except Exception as e:
-            self.fail(e)
-
-        # wait for schema to refetch
-        await self.sleep(1)
-
-        self.assertGreater(self.conn.schema_id, schema_before,
-                           'Schema changed')
+            try:
+                await self.conn.ping()
+            except Exception as e:
+                self.fail(e)
+
+            # wait for schema to refetch
+            await self.sleep(1)
+
+            self.assertGreater(self.conn.schema_id, schema_before,
+                               'Schema changed')
+            self.assertIn('new_space', self.conn.schema.spaces)
+        finally:
+            await self.conn.eval(
+                "local s = box.space.new_space;"
+                "if s ~= nil then s:drop(); end"
+            )
 
     async def test__schema_refetch_manual(self):
         await self.tnt_reconnect(fetch_schema=True,
@@ -82,6 +87,10 @@ async def test__schema_refetch_manual(self):
 
         self.assertGreater(self.conn.schema_id, schema_before,
                            'Schema changed')
+        sp = self.conn.schema.spaces[self.TESTER_SPACE_NAME]
+        self.assertEqual(6, len(sp.metadata.fields))
+        self.assertEqual('f6', sp.metadata.fields[5].name)
+        self.assertEqual('*', sp.metadata.fields[5].type)
 
     async def test__schema_no_fetch_and_refetch(self):
         await self.tnt_reconnect(auto_refetch_schema=False,
@@ -182,7 +191,8 @@ class A:
             pass
 
         with self.assertRaisesRegex(
-            TypeError, 'Type `(.+)` is not supported for encoding'):
+            TypeError, 'Type `(.+)` is not supported for encoding'
+        ):
             await self.conn.call('func_param', [{'a': A()}])
 
     async def test__schema_refetch_next_byte(self):
@@ -248,6 +258,8 @@ async def test__schema_refetch_on_disconnect_race_condition(self):
                                    username='t1', password='t1')
         await conn.connect()
         await conn.eval("require('msgpack').cfg{encode_use_tostring = true}")
-        await conn.call('box.schema.space.create', ['geo', {"if_not_exists": True}])
-        await conn.call('box.space.geo:format', [[{"name": "id", "type": "string"}]])
+        await conn.call('box.schema.space.create',
+                        ['geo', {"if_not_exists": True}])
+        await conn.call('box.space.geo:format',
+                        [[{"name": "id", "type": "string"}]])
         await conn.disconnect()
diff --git a/tests/test_connect.py b/tests/test_connect.py
index 7bf44a1..04f2366 100644
--- a/tests/test_connect.py
+++ b/tests/test_connect.py
@@ -22,7 +22,7 @@ async def test__connect(self):
         self.assertIsNone(conn.password)
         self.assertEqual(conn.reconnect_timeout, 0)
         self.assertEqual(conn.connect_timeout, 3)
-        self.assertEqual(conn.loop, self.loop)
+        self.assertEqual(conn.loop, None)
         self.assertIsNone(conn.initial_read_buffer_size)
         self.assertIsNone(conn.schema_id)
         self.assertIsNone(conn.version)
@@ -36,6 +36,7 @@ async def test__connect(self):
         self.assertEqual(conn.state, ConnectionState.DISCONNECTED)
 
         c = await conn.connect()
+        self.assertEqual(conn.loop, self.loop)
         self.assertEqual(c, conn)
         self.assertIsNotNone(conn._transport)
         self.assertIsNotNone(conn._protocol)
@@ -45,6 +46,9 @@ async def test__connect(self):
         self.assertIsNotNone(conn._protocol.schema)
         self.assertIsNotNone(conn.version)
 
+        self.assertIsNotNone(conn.schema.id)
+        self.assertIsNotNone(conn.schema.spaces)
+
         await conn.call('box.info')
         await conn.disconnect()
 
diff --git a/tests/test_mp_ext.py b/tests/test_mp_ext.py
new file mode 100644
index 0000000..7709291
--- /dev/null
+++ b/tests/test_mp_ext.py
@@ -0,0 +1,265 @@
+import datetime
+import sys
+import uuid
+from decimal import Decimal
+
+import dateutil.parser
+import pytz
+
+from asynctnt import IProtoError
+from asynctnt._testbase import ensure_version
+from asynctnt.exceptions import TarantoolDatabaseError, ErrorCode
+from tests import BaseTarantoolTestCase
+
+
+class MpExtTestCase(BaseTarantoolTestCase):
+
+    @ensure_version(min=(2, 2))
+    async def test__decimal(self):
+        space = 'tester_ext_dec'
+
+        dec = Decimal('-12.34')
+        res = await self.conn.replace(space, [1, dec])
+        self.assertEqual(res[0][1], dec)
+
+        dec = Decimal('-12.345')
+        res = await self.conn.replace(space, [1, dec])
+        self.assertEqual(res[0][1], dec)
+
+        dec = Decimal('-12.4')
+        res = await self.conn.replace(space, [1, dec])
+        self.assertEqual(res[0][1], dec)
+
+        dec = Decimal('0')
+        res = await self.conn.replace(space, [1, dec])
+        self.assertEqual(res[0][1], dec)
+
+        dec = Decimal('0.000')
+        res = await self.conn.replace(space, [1, dec])
+        self.assertEqual(res[0][1], dec)
+
+        dec = Decimal(42)
+        res = await self.conn.replace(space, [1, dec])
+        self.assertEqual(res[0][1], dec)
+
+        dec = Decimal('0.33')
+        res = await self.conn.replace(space, [1, dec])
+        self.assertEqual(res[0][1], dec)
+
+        dec = Decimal('0.000000000000000000000000000000000010')
+        res = await self.conn.replace(space, [1, dec])
+        self.assertEqual(res[0][1], dec)
+
+        dec = Decimal('-0.000000000000000000000000000000000010')
+        res = await self.conn.replace(space, [1, dec])
+        self.assertEqual(res[0][1], dec)
+
+        dec = Decimal('0.1111111111111111')
+        res = await self.conn.replace(space, [1, dec])
+        self.assertEqual(res[0][1], dec)
+
+        dec = Decimal('-0.1111111111111111')
+        res = await self.conn.replace(space, [1, dec])
+        self.assertEqual(res[0][1], dec)
+
+        dec = Decimal('-0.111111')
+        res = await self.conn.replace(space, [1, dec])
+        self.assertEqual(res[0][1], dec)
+
+    @ensure_version(min=(2, 4, 1))
+    async def test__uuid(self):
+        space = 'tester_ext_uuid'
+
+        val = uuid.uuid4()
+        res = await self.conn.replace(space, [1, val])
+        self.assertEqual(res[0][1], val)
+
+        val = uuid.UUID('f6423bdf-b49e-4913-b361-0740c9702e4b')
+        res = await self.conn.replace(space, [1, val])
+        self.assertEqual(res[0][1], val)
+
+        val = uuid.UUID('00000000-0000-0000-0000-000000000000')
+        res = await self.conn.replace(space, [1, val])
+        self.assertEqual(res[0][1], val)
+
+        val = uuid.uuid1(1, 100)
+        res = await self.conn.replace(space, [1, val])
+        self.assertEqual(res[0][1], val)
+
+        val = uuid.uuid3(uuid.uuid4(), "hellothere")
+        res = await self.conn.replace(space, [1, val])
+        self.assertEqual(res[0][1], val)
+
+        val = uuid.uuid5(uuid.NAMESPACE_URL, "generalkenobi")
+        res = await self.conn.replace(space, [1, val])
+        self.assertEqual(res[0][1], val)
+
+    @ensure_version(min=(2, 4, 1))
+    async def test__ext_error(self):
+        try:
+            await self.conn.eval("""
+                box.schema.space.create('_space')
+            """)
+        except TarantoolDatabaseError as e:
+            self.assertIsNotNone(e.error)
+            self.assertGreater(len(e.error.trace), 0)
+            frame = e.error.trace[0]
+            self.assertEqual('ClientError', frame.error_type)
+            self.assertIsNotNone(frame.file)
+            self.assertIsNotNone(frame.line)
+            self.assertEqual("Space '_space' already exists", frame.message)
+            self.assertEqual(0, frame.err_no)
+            self.assertEqual(ErrorCode.ER_SPACE_EXISTS, frame.code)
+
+    @ensure_version(min=(2, 4, 1))
+    async def test__ext_error_custom(self):
+        try:
+            await self.conn.eval("""
+                local e = box.error.new{code=5,reason='A',type='B'}
+                box.error(e)
+            """)
+        except TarantoolDatabaseError as e:
+            self.assertIsNotNone(e.error)
+            self.assertGreater(len(e.error.trace), 0)
+            frame = e.error.trace[0]
+            self.assertEqual('CustomError', frame.error_type)
+            self.assertIsNotNone(frame.file)
+            self.assertIsNotNone(frame.line)
+            self.assertEqual("A", frame.message)
+            self.assertEqual(0, frame.err_no)
+            self.assertEqual(5, frame.code)
+            self.assertIn('custom_type', frame.fields)
+            self.assertEqual('B', frame.fields['custom_type'])
+
+    @ensure_version(min=(2, 10))
+    async def test__ext_error_custom_return(self):
+        resp = await self.conn.eval("""
+            local e = box.error.new{code=5,reason='A',type='B'}
+            return e
+        """)
+        e = resp[0]
+        self.assertIsInstance(e, IProtoError)
+        self.assertGreater(len(e.trace), 0)
+        frame = e.trace[0]
+        self.assertEqual('CustomError', frame.error_type)
+        self.assertEqual('eval', frame.file)
+        self.assertEqual(2, frame.line)
+        self.assertEqual("A", frame.message)
+        self.assertEqual(0, frame.err_no)
+        self.assertEqual(5, frame.code)
+        self.assertIn('custom_type', frame.fields)
+        self.assertEqual('B', frame.fields['custom_type'])
+
+    @ensure_version(min=(2, 10))
+    async def test__ext_error_custom_return_with_disabled_exterror(self):
+        await self.conn.eval("""
+            require('msgpack').cfg{encode_error_as_ext = false}
+        """)
+        try:
+            resp = await self.conn.eval("""
+                local e = box.error.new{code=5,reason='A',type='B'}
+                return e
+            """)
+            e = resp[0]
+            self.assertIsInstance(e, str)
+            self.assertEqual('A', e)
+        finally:
+            await self.conn.eval("""
+                require('msgpack').cfg{encode_error_as_ext = true}
+            """)
+
+    @ensure_version(min=(2, 10))
+    async def test__ext_datetime_read(self):
+        resp = await self.conn.eval("""
+            local date = require('datetime')
+            return date.parse('2000-01-01T02:00:00.23+0300')
+        """)
+        res = resp[0]
+        dt = datetime_fromisoformat('2000-01-01T02:00:00.230000+03:00')
+        self.assertEqual(dt, res)
+
+    @ensure_version(min=(2, 10))
+    async def test__ext_datetime_tz(self):
+        resp = await self.conn.eval("""
+            local date = require('datetime')
+            return date.parse('2000-01-01T02:00:00 MSK')
+        """)
+        res = resp[0]
+        dt = datetime_fromisoformat('2000-01-01T02:00:00+03:00')
+        self.assertEqual(dt, res)
+
+    @ensure_version(min=(2, 10))
+    async def test__ext_datetime_read_neg_tz(self):
+        resp = await self.conn.eval("""
+            local date = require('datetime')
+            return date.parse('2000-01-01T02:17:43.23-08:00')
+        """)
+        res = resp[0]
+        dt = datetime_fromisoformat('2000-01-01T02:17:43.230000-08:00')
+        self.assertEqual(dt, res)
+
+    @ensure_version(min=(2, 10))
+    async def test__ext_datetime_read_before_1970(self):
+        resp = await self.conn.eval("""
+            local date = require('datetime')
+            return date.parse('1930-01-01T02:17:43.23-08:00')
+        """)
+        res = resp[0]
+        dt = datetime_fromisoformat('1930-01-01T02:17:43.230000-08:00')
+        self.assertEqual(dt, res)
+
+    @ensure_version(min=(2, 10))
+    async def test__ext_datetime_write(self):
+        sp = 'tester_ext_datetime'
+        dt = datetime_fromisoformat('2000-01-01T02:17:43.230000-08:00')
+        resp = await self.conn.insert(sp, [1, dt])
+        res = resp[0]
+        self.assertEqual(dt, res['dt'])
+
+    @ensure_version(min=(2, 10))
+    async def test__ext_datetime_write_before_1970(self):
+        sp = 'tester_ext_datetime'
+        dt = datetime_fromisoformat('1004-01-01T02:17:43.230000+04:00')
+        resp = await self.conn.insert(sp, [1, dt])
+        res = resp[0]
+        self.assertEqual(dt, res['dt'])
+
+    @ensure_version(min=(2, 10))
+    async def test__ext_datetime_write_without_tz(self):
+        sp = 'tester_ext_datetime'
+        dt = datetime_fromisoformat('2022-04-23T02:17:43.450000')
+        resp = await self.conn.insert(sp, [1, dt])
+        res = resp[0]
+        self.assertEqual(dt, res['dt'])
+
+    @ensure_version(min=(2, 10))
+    async def test__ext_datetime_write_without_tz_integer(self):
+        sp = 'tester_ext_datetime'
+        dt = datetime_fromisoformat('2022-04-23T02:17:43')
+        resp = await self.conn.insert(sp, [1, dt])
+        res = resp[0]
+        self.assertEqual(dt, res['dt'])
+
+    @ensure_version(min=(2, 10))
+    async def test__ext_datetime_write_pytz(self):
+        sp = 'tester_ext_datetime'
+        dt = datetime_fromisoformat('2022-04-23T02:17:43')
+        dt = pytz.timezone('Europe/Amsterdam').localize(dt)
+        resp = await self.conn.insert(sp, [1, dt])
+        res = resp[0]
+        self.assertEqual(dt, res['dt'])
+
+    @ensure_version(min=(2, 10))
+    async def test__ext_datetime_write_pytz_america(self):
+        sp = 'tester_ext_datetime'
+        dt = datetime_fromisoformat('2022-04-23T02:17:43')
+        dt = pytz.timezone('America/New_York').localize(dt)
+        resp = await self.conn.insert(sp, [1, dt])
+        res = resp[0]
+        self.assertEqual(dt, res['dt'])
+
+
+def datetime_fromisoformat(s):
+    if sys.version_info < (3, 7, 0):
+        return dateutil.parser.isoparse(s)
+    return datetime.datetime.fromisoformat(s)
diff --git a/tests/test_op_push.py b/tests/test_op_push.py
index fdb893d..1a4969c 100644
--- a/tests/test_op_push.py
+++ b/tests/test_op_push.py
@@ -3,7 +3,8 @@
 import asynctnt
 from asynctnt import Response, PushIterator
 from asynctnt._testbase import ensure_version
-from asynctnt.exceptions import TarantoolDatabaseError, TarantoolNotConnectedError
+from asynctnt.exceptions import TarantoolDatabaseError, \
+    TarantoolNotConnectedError
 from tests import BaseTarantoolTestCase
 
 
diff --git a/tests/test_op_sql.py b/tests/test_op_sql.py
deleted file mode 100644
index 5f101cb..0000000
--- a/tests/test_op_sql.py
+++ /dev/null
@@ -1,95 +0,0 @@
-from asynctnt import Response
-from asynctnt._testbase import ensure_version
-from tests import BaseTarantoolTestCase
-
-
-class SQLTestCase(BaseTarantoolTestCase):
-    @ensure_version(min=(2, 0))
-    async def test__sql_basic(self):
-        res = await self.conn.sql('select 1, 2')
-
-        self.assertIsInstance(res, Response, 'Got response')
-        self.assertEqual(res.code, 0, 'success')
-        self.assertGreater(res.sync, 0, 'sync > 0')
-        self.assertResponseEqual(res, [[1, 2]], 'Body ok')
-
-    @ensure_version(min=(2, 0))
-    async def test__sql_with_param(self):
-        res = await self.conn.sql('select 1, 2 where 1 = ?', [1])
-
-        self.assertResponseEqual(res, [[1, 2]], 'Body ok')
-
-    @ensure_version(min=(2, 0))
-    async def test__sql_with_param_cols(self):
-        res = await self.conn.sql('select 1 as a, 2 as b where 1 = ?', [1])
-
-        self.assertResponseEqualKV(res, [{'A': 1, 'B': 2}], 'Body ok')
-
-    @ensure_version(min=(2, 0))
-    async def test__sql_insert(self):
-        res = await self.conn.sql(
-            "insert into sql_space (id, name) values (1, 'one')")
-        self.assertEqual(1, res.rowcount, 'rowcount ok')
-
-    @ensure_version(min=(2, 0))
-    async def test__sql_empty_autoincrement(self):
-        res = await self.conn.sql(
-            "insert into sql_space (id, name) values (1, 'one')")
-        self.assertEqual(None, res.autoincrement_ids, 'autoincrement ok')
-
-    @ensure_version(min=(2, 0))
-    async def test__sql_insert_autoincrement(self):
-        res = await self.conn.sql(
-            "insert into sql_space_autoincrement (name) values ('name')")
-        self.assertEqual(1, res.rowcount, 'rowcount ok')
-        self.assertEqual([1], res.autoincrement_ids, 'autoincrement ok')
-
-    @ensure_version(min=(2, 0))
-    async def test__sql_insert_autoincrement_multiple(self):
-        res = await self.conn.sql(
-            "insert into sql_space_autoincrement_multiple (name) values ('name'), ('name2')")
-        self.assertEqual(2, res.rowcount, 'rowcount ok')
-        self.assertEqual([1, 2], res.autoincrement_ids, 'autoincrement ok')
-
-    @ensure_version(min=(2, 0))
-    async def test__sql_insert_multiple(self):
-        res = await self.conn.sql(
-            "insert into sql_space (id, name) "
-            "values (1, 'one'), (2, 'two')")
-        self.assertEqual(2, res.rowcount, 'rowcount ok')
-
-    @ensure_version(min=(2, 0))
-    async def test__sql_update(self):
-        await self.conn.sql("insert into sql_space values (1, 'one')")
-
-        res = await self.conn.sql(
-            "update sql_space set name = 'uno' where id = 1")
-
-        self.assertEqual(1, res.rowcount, 'rowcount ok')
-
-    @ensure_version(min=(2, 0))
-    async def test__sql_update_multiple(self):
-        await self.conn.sql("insert into sql_space values (1, 'one')")
-        await self.conn.sql("insert into sql_space values (2, 'two')")
-
-        res = await self.conn.sql("update sql_space set name = 'uno'")
-
-        self.assertEqual(2, res.rowcount, 'rowcount ok')
-
-    @ensure_version(min=(2, 0))
-    async def test__sql_delete(self):
-        await self.conn.sql("insert into sql_space values (1, 'one')")
-        res = await self.conn.sql("delete from sql_space where name = 'one'")
-        self.assertEqual(1, res.rowcount, 'rowcount ok')
-
-    @ensure_version(min=(2, 0))
-    async def test__sql_delete_multiple(self):
-        await self.conn.sql("insert into sql_space values (1, 'one')")
-        await self.conn.sql("insert into sql_space values (2, 'two')")
-
-        res = await self.conn.sql("delete from sql_space")
-        # TODO: wait for https://github.com/tarantool/tarantool/issues/3816
-        # self.assertEqual(2, res.rowcount, 'rowcount ok')
-
-        res = await self.conn.sql("select * from sql_space")
-        self.assertEqual(0, res.rowcount, 'rowcount is surely ok')
diff --git a/tests/test_op_sql_execute.py b/tests/test_op_sql_execute.py
new file mode 100644
index 0000000..fe62765
--- /dev/null
+++ b/tests/test_op_sql_execute.py
@@ -0,0 +1,216 @@
+from asynctnt import Response
+from asynctnt._testbase import ensure_version
+from tests import BaseTarantoolTestCase
+
+
+class SQLExecuteTestCase(BaseTarantoolTestCase):
+    @ensure_version(min=(2, 0))
+    async def test__sql_basic(self):
+        res = await self.conn.execute('select 1, 2')
+
+        self.assertIsInstance(res, Response, 'Got response')
+        self.assertEqual(res.code, 0, 'success')
+        self.assertGreater(res.sync, 0, 'sync > 0')
+        self.assertResponseEqual(res, [[1, 2]], 'Body ok')
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_with_param(self):
+        res = await self.conn.execute('select 1, 2 where 1 = ?', [1])
+
+        self.assertResponseEqual(res, [[1, 2]], 'Body ok')
+
+    @ensure_version(min=(2, 0))
+    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')
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_with_param_cols2(self):
+        res = await self.conn.execute(
+            'select 1 as a, 2 as b where 1 = ? and 2 = ?', [1, 2])
+
+        self.assertResponseEqualKV(res, [{'A': 1, 'B': 2}], 'Body ok')
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_with_param_cols_maps(self):
+        res = await self.conn.execute(
+            'select 1 as a, 2 as b where 1 = :p1 and 2 = :p2', [
+                {':p1': 1},
+                {':p2': 2},
+            ])
+
+        self.assertResponseEqualKV(res, [{'A': 1, '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 = ?', [
+                {':p1': 1},
+                {':p2': 2},
+                3,
+                4
+            ])
+
+        self.assertResponseEqualKV(res, [{'A': 1, 'B': 2}], 'Body ok')
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_insert(self):
+        res = await self.conn.execute(
+            "insert into sql_space (id, name) values (1, 'one')")
+        self.assertEqual(1, res.rowcount, 'rowcount ok')
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_empty_autoincrement(self):
+        res = await self.conn.execute(
+            "insert into sql_space (id, name) values (1, 'one')")
+        self.assertEqual(None, res.autoincrement_ids, 'autoincrement ok')
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_insert_autoincrement(self):
+        res = await self.conn.execute(
+            "insert into sql_space_autoincrement (name) values ('name')")
+        self.assertEqual(1, res.rowcount, 'rowcount ok')
+        self.assertEqual([1], res.autoincrement_ids, 'autoincrement ok')
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_insert_autoincrement_multiple(self):
+        res = await self.conn.execute(
+            "insert into sql_space_autoincrement_multiple (name) "
+            "values ('name'), ('name2')"
+        )
+        self.assertEqual(2, res.rowcount, 'rowcount ok')
+        self.assertEqual([1, 2], res.autoincrement_ids, 'autoincrement ok')
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_insert_multiple(self):
+        res = await self.conn.execute(
+            "insert into sql_space (id, name) "
+            "values (1, 'one'), (2, 'two')")
+        self.assertEqual(2, res.rowcount, 'rowcount ok')
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_update(self):
+        await self.conn.execute("insert into sql_space values (1, 'one')")
+
+        res = await self.conn.execute(
+            "update sql_space set name = 'uno' where id = 1")
+
+        self.assertEqual(1, res.rowcount, 'rowcount ok')
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_update_multiple(self):
+        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("update sql_space set name = 'uno'")
+
+        self.assertEqual(2, res.rowcount, 'rowcount ok')
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_delete(self):
+        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.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'])
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_delete_multiple(self):
+        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("delete from sql_space")
+        self.assertEqual(2, res.rowcount, 'rowcount ok')
+
+        res = await self.conn.execute("select * from sql_space")
+        self.assertEqual(0, res.rowcount, 'rowcount is surely ok')
+
+    @ensure_version(min=(2, 0))
+    async def test__metadata(self):
+        res = await self.conn.execute('select 1, 2')
+        self.assertIsNotNone(res.metadata)
+        self.assertIsNotNone(res.metadata.fields)
+        self.assertEqual(2, len(res.metadata.fields))
+
+        self.assertEqual('COLUMN_1', res.metadata.fields[0].name)
+        self.assertEqual('integer', res.metadata.fields[0].type)
+        self.assertEqual('COLUMN_2', res.metadata.fields[1].name)
+        self.assertEqual('integer', res.metadata.fields[1].type)
+
+    @ensure_version(min=(2, 0))
+    async def test__metadata_names(self):
+        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('integer', res.metadata.fields[0].type)
+        self.assertEqual('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.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('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('string', res.metadata.fields[1].type)
+        self.assertIsNone(res.metadata.fields[1].is_nullable)
+        self.assertIsNone(res.metadata.fields[1].is_autoincrement)
+        self.assertIsNone(res.metadata.fields[1].collation)
+
+    @ensure_version(min=(2, 0))
+    async def test__sql_select_full_metadata(self):
+        await self.conn.execute("insert into sql_space values (1, 'one')")
+        await self.conn.execute("insert into sql_space values (2, 'two')")
+
+        await self.conn.update(
+            '_session_settings',
+            ['sql_full_metadata'],
+            [('=', 'value', True)]
+        )
+
+        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('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('string', res.metadata.fields[1].type)
+            self.assertEqual(True, res.metadata.fields[1].is_nullable)
+            self.assertEqual(None, res.metadata.fields[1].is_autoincrement)
+            self.assertEqual('unicode', res.metadata.fields[1].collation)
+        finally:
+            await self.conn.update(
+                '_session_settings',
+                ['sql_full_metadata'],
+                [('=', 'value', False)]
+            )
diff --git a/tests/test_op_sql_prepared.py b/tests/test_op_sql_prepared.py
new file mode 100644
index 0000000..6a18d4f
--- /dev/null
+++ b/tests/test_op_sql_prepared.py
@@ -0,0 +1,97 @@
+from asynctnt import Response
+from asynctnt._testbase import ensure_version
+from asynctnt.prepared import PreparedStatement
+from tests import BaseTarantoolTestCase
+
+
+class SQLPreparedStatementTestCase(BaseTarantoolTestCase):
+    @ensure_version(min=(2, 0))
+    async def test__basic(self):
+        stmt = self.conn.prepare('select 1, 2')
+        self.assertIsInstance(stmt, PreparedStatement, 'Got correct instance')
+        async with stmt:
+            self.assertIsNotNone(stmt.id, 'statement has been prepared')
+
+            res = await stmt.execute()
+            self.assertIsInstance(res, Response, 'Got response')
+            self.assertEqual(res.code, 0, 'success')
+            self.assertGreater(res.sync, 0, 'sync > 0')
+            self.assertResponseEqual(res, [[1, 2]], 'Body ok')
+
+        self.assertIsNone(stmt.id, 'statement has been unprepared')
+
+    @ensure_version(min=(2, 0))
+    async def test__manual(self):
+        stmt = self.conn.prepare('select 1, 2')
+        self.assertIsInstance(stmt, PreparedStatement, 'Got correct instance')
+        stmt_id = await stmt.prepare()
+        self.assertIsNotNone(stmt_id, 'statement has been prepared')
+        res = await stmt.execute()
+        self.assertIsInstance(res, Response, 'Got response')
+        self.assertEqual(res.code, 0, 'success')
+        self.assertGreater(res.sync, 0, 'sync > 0')
+        self.assertResponseEqual(res, [[1, 2]], 'Body ok')
+        await stmt.unprepare()
+
+    @ensure_version(min=(2, 0))
+    async def test__manual_iproto(self):
+        res = await self.conn.prepare_iproto('select 1, 2')
+        self.assertEqual(res.code, 0, 'success')
+        stmt_id = res.stmt_id
+        self.assertNotEqual(stmt_id, 0, 'received statement_id')
+
+        res = await self.conn.execute(stmt_id, [])
+        self.assertIsInstance(res, Response, 'Got response')
+        self.assertEqual(res.code, 0, 'success')
+        self.assertGreater(res.sync, 0, 'sync > 0')
+        self.assertResponseEqual(res, [[1, 2]], 'Body ok')
+
+        res = await self.conn.unprepare_iproto(stmt_id)
+        self.assertEqual(res.code, 0, 'success')
+
+    @ensure_version(min=(2, 0))
+    async def test__bind(self):
+        stmt = self.conn.prepare('select 1, 2 where 1 = ? and 2 = ?')
+        async with stmt:
+            res = await stmt.execute([1, 2])
+            self.assertIsInstance(res, Response, 'Got response')
+            self.assertEqual(res.code, 0, 'success')
+            self.assertGreater(res.sync, 0, 'sync > 0')
+            self.assertResponseEqual(res, [[1, 2]], 'Body ok')
+
+    @ensure_version(min=(2, 0))
+    async def test__bind_metadata(self):
+        stmt = self.conn.prepare('select 1, 2 where 1 = :a and 2 = :b')
+        async with stmt:
+            self.assertIsNotNone(stmt.params_count)
+            self.assertIsNotNone(stmt.params)
+
+            self.assertEqual(2, stmt.params_count)
+            self.assertEqual(':a', stmt.params.fields[0].name)
+            self.assertEqual('ANY', stmt.params.fields[0].type)
+            self.assertEqual(':b', stmt.params.fields[1].name)
+            self.assertEqual('ANY', stmt.params.fields[1].type)
+
+    @ensure_version(min=(2, 0))
+    async def test__bind_2_execute(self):
+        stmt = self.conn.prepare('select 1, 2 where 1 = ? and 2 = ?')
+        async with stmt:
+            res = await stmt.execute([1, 2])
+            self.assertIsInstance(res, Response, 'Got response')
+            self.assertEqual(res.code, 0, 'success')
+            self.assertGreater(res.sync, 0, 'sync > 0')
+            self.assertResponseEqual(res, [[1, 2]], 'Body ok')
+
+            res = await stmt.execute([3, 4])
+            self.assertIsInstance(res, Response, 'Got response')
+            self.assertEqual(res.code, 0, 'success')
+            self.assertGreater(res.sync, 0, 'sync > 0')
+            self.assertResponseEqual(res, [], 'Body is empty')
+
+    @ensure_version(min=(2, 0))
+    async def test__context_manager_double_enter(self):
+        stmt = self.conn.prepare('select 1, 2 where 1 = ? and 2 = ?')
+        async with stmt:
+            async with stmt:  # does nothing
+                res = await stmt.execute([1, 2])
+                self.assertResponseEqual(res, [[1, 2]], 'Body ok')
diff --git a/tests/test_response.py b/tests/test_response.py
index c03ff0e..c0d3378 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -198,3 +198,79 @@ async def test__response_str(self):
             str(res)
         except Exception as e:
             self.fail(e)
+
+    async def test__metadata(self):
+        self.assertIsNotNone(self.conn.schema.id)
+        self.assertIsNotNone(self.conn.schema.spaces)
+        self.assertIn(self.TESTER_SPACE_NAME, self.conn.schema.spaces)
+        self.assertIn(self.TESTER_SPACE_ID, self.conn.schema.spaces)
+        self.assertIs(self.conn.schema.spaces[self.TESTER_SPACE_NAME],
+                      self.conn.schema.spaces[self.TESTER_SPACE_ID])
+
+        sp = self.conn.schema.spaces[self.TESTER_SPACE_NAME]
+        self.assertEqual(self.TESTER_SPACE_NAME, sp.name)
+        self.assertEqual(self.TESTER_SPACE_ID, sp.sid)
+        self.assertEqual('memtx', sp.engine)
+        self.assertEqual(4, len(sp.indexes))
+        self.assertIn('primary', sp.indexes)
+        self.assertIn(0, sp.indexes)
+        self.assertIn('txt', sp.indexes)
+        self.assertIn(1, sp.indexes)
+        self.assertEqual(5, len(sp.metadata.fields))
+        self.assertEqual('f1', sp.metadata.fields[0].name)
+        self.assertEqual('unsigned', sp.metadata.fields[0].type)
+        self.assertEqual('f2', sp.metadata.fields[1].name)
+        self.assertEqual('string', sp.metadata.fields[1].type)
+        self.assertEqual('f5', sp.metadata.fields[4].name)
+        self.assertEqual('*', sp.metadata.fields[4].type)
+        self.assertEqual(5, len(sp.metadata.name_id_map))
+        self.assertEqual(0, sp.metadata.name_id_map['f1'])
+
+        idx = sp.indexes[0]
+        self.assertEqual(0, idx.iid)
+        self.assertEqual('primary', idx.name)
+        self.assertEqual(self.TESTER_SPACE_ID, idx.sid)
+        self.assertEqual('tree', idx.index_type)
+        self.assertEqual(1, len(idx.metadata.fields))
+        self.assertEqual('f1', idx.metadata.fields[0].name)
+        self.assertEqual('unsigned', idx.metadata.fields[0].type)
+        self.assertEqual(1, len(idx.metadata.name_id_map))
+        self.assertEqual(0, idx.metadata.name_id_map['f1'])
+
+    async def test__metadata_is_nullable(self):
+        sp_name = "test_space_with_nullable"
+        await self.conn.eval("""
+            local s = box.schema.space.create('%s')
+            s:format({
+                {name = "id", type = "unsigned"},
+                {name = "name", type = "string", is_nullable = true},
+            })
+        """ % (sp_name,))
+
+        try:
+            # just to be sure that schema is refreshed
+            await self.conn.refetch_schema()
+
+            self.assertIn(sp_name, self.conn.schema.spaces)
+            sp = self.conn.schema.spaces[sp_name]
+            self.assertEqual(sp_name, sp.name)
+            self.assertEqual('memtx', sp.engine)
+            self.assertEqual(0, len(sp.indexes))
+            self.assertEqual(2, len(sp.metadata.fields))
+            self.assertEqual('id', sp.metadata.fields[0].name)
+            self.assertEqual('unsigned', sp.metadata.fields[0].type)
+            self.assertEqual(None, sp.metadata.fields[0].is_nullable)
+            self.assertEqual('name', sp.metadata.fields[1].name)
+            self.assertEqual('string', sp.metadata.fields[1].type)
+            self.assertEqual(True, sp.metadata.fields[1].is_nullable)
+            self.assertEqual(2, len(sp.metadata.name_id_map))
+            self.assertEqual(0, sp.metadata.name_id_map['id'])
+            self.assertEqual(1, sp.metadata.name_id_map['name'])
+
+        finally:
+            await self.conn.eval("""
+                local s = box.space['%s']
+                if s ~= nil then
+                    s:drop()
+                end
+            """ % (sp_name,))
diff --git a/tests/test_stream.py b/tests/test_stream.py
new file mode 100644
index 0000000..06ab095
--- /dev/null
+++ b/tests/test_stream.py
@@ -0,0 +1,182 @@
+import asyncio
+
+from asynctnt._testbase import ensure_version, ensure_bin_version
+from asynctnt.exceptions import TarantoolDatabaseError, ErrorCode
+from tests import BaseTarantoolTestCase
+
+
+@ensure_bin_version(min=(2, 10))
+class StreamTestCase(BaseTarantoolTestCase):
+    EXTRA_BOX_CFG = "memtx_use_mvcc_engine = true"
+
+    @ensure_version(min=(2, 10))
+    async def test__transaction_commit(self):
+        s = self.conn.stream()
+        self.assertGreater(s.stream_id, 0)
+
+        await s.begin()
+        data = [1, 'hello', 1, 4, 'what is up']
+        await s.insert(self.TESTER_SPACE_NAME, data)
+        res = await s.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data])
+
+        await s.commit()
+
+        res = await self.conn.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data])
+
+    @ensure_version(min=(2, 10))
+    async def test__transaction_rolled_back(self):
+        s = self.conn.stream()
+        await s.begin()
+        data = [1, 'hello', 1, 4, 'what is up']
+        await s.insert(self.TESTER_SPACE_NAME, data)
+        res = await s.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data])
+
+        await s.rollback()
+
+        res = await self.conn.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [])
+
+    @ensure_version(min=(2, 10))
+    async def test__transaction_begin_through_call(self):
+        s = self.conn.stream()
+        await s.call('box.begin')
+        data = [1, 'hello', 1, 4, 'what is up']
+        await s.insert(self.TESTER_SPACE_NAME, data)
+        res = await s.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data])
+
+        await s.commit()
+
+        res = await self.conn.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data])
+
+    @ensure_version(min=(2, 10))
+    async def test__transaction_commit_through_call(self):
+        s = self.conn.stream()
+        await s.begin()
+        data = [1, 'hello', 1, 4, 'what is up']
+        await s.insert(self.TESTER_SPACE_NAME, data)
+        res = await s.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data])
+
+        await s.call('box.commit')
+
+        res = await self.conn.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data])
+
+    @ensure_version(min=(2, 10))
+    async def test__transaction_rolled_back_through_call(self):
+        s = self.conn.stream()
+        await s.begin()
+        data = [1, 'hello', 1, 4, 'what is up']
+        await s.insert(self.TESTER_SPACE_NAME, data)
+        res = await s.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data])
+
+        await s.call('box.rollback')
+
+        res = await self.conn.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [])
+
+    @ensure_version(min=(2, 10))
+    async def test__transaction_commit_through_sql(self):
+        s = self.conn.stream()
+        await s.execute('START TRANSACTION')
+        data = [1, 'hello', 1, 4, 'what is up']
+        await s.insert(self.TESTER_SPACE_NAME, data)
+        res = await s.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data])
+
+        await s.execute('COMMIT')
+
+        res = await self.conn.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data])
+
+    @ensure_version(min=(2, 10))
+    async def test__transaction_rollback_through_sql(self):
+        s = self.conn.stream()
+        await s.execute('START TRANSACTION')
+        data = [1, 'hello', 1, 4, 'what is up']
+        await s.insert(self.TESTER_SPACE_NAME, data)
+        res = await s.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data])
+
+        await s.execute('ROLLBACK')
+
+        res = await self.conn.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [])
+
+    @ensure_version(min=(2, 10))
+    async def test__transaction_context_manager_commit(self):
+        data = [1, 'hello', 1, 4, 'what is up']
+
+        async with self.conn.stream() as s:
+            await s.insert(self.TESTER_SPACE_NAME, data)
+            res = await s.select(self.TESTER_SPACE_NAME)
+            self.assertResponseEqual(res, [data])
+
+        res = await self.conn.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data])
+
+    @ensure_version(min=(2, 10))
+    async def test__transaction_context_manager_rollback(self):
+        class ExpectedError(Exception):
+            pass
+
+        data = [1, 'hello', 1, 4, 'what is up']
+
+        try:
+            async with self.conn.stream() as s:
+                await s.insert(self.TESTER_SPACE_NAME, data)
+                res = await s.select(self.TESTER_SPACE_NAME)
+                self.assertResponseEqual(res, [data])
+
+                raise ExpectedError("some error")
+        except ExpectedError:
+            pass
+
+        res = await self.conn.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [])
+
+    @ensure_version(min=(2, 10))
+    async def test__transaction_2_streams(self):
+        data1 = [1, 'hello', 1, 4, 'what is up']
+        data2 = [2, 'hi', 100, 400, 'nothing match']
+
+        s1 = self.conn.stream()
+        s2 = self.conn.stream()
+
+        await s1.begin()
+        await s2.begin()
+
+        await s1.insert(self.TESTER_SPACE_NAME, data1)
+        await s2.insert(self.TESTER_SPACE_NAME, data2)
+
+        res = await s1.select(self.TESTER_SPACE_NAME, [1])
+        self.assertResponseEqual(res, [data1])
+
+        res = await s2.select(self.TESTER_SPACE_NAME, [2])
+        self.assertResponseEqual(res, [data2])
+
+        await s1.commit()
+        await s2.commit()
+
+        res = await self.conn.select(self.TESTER_SPACE_NAME)
+        self.assertResponseEqual(res, [data1, data2])
+
+    @ensure_version(min=(2, 10))
+    async def test__transaction_timeout(self):
+        s = self.conn.stream()
+        await s.begin(tx_timeout=0.5)
+
+        await asyncio.sleep(1.0)
+
+        with self.assertRaises(TarantoolDatabaseError) as exc:
+            await s.commit()
+
+        self.assertEqual(ErrorCode.ER_TRANSACTION_TIMEOUT, exc.exception.code)
+        self.assertEqual('Transaction has been aborted by timeout',
+                         exc.exception.message)