diff --git a/.github/workflows/packing.yml b/.github/workflows/packing.yml index e413d4e1..d76a48ef 100644 --- a/.github/workflows/packing.yml +++ b/.github/workflows/packing.yml @@ -99,6 +99,12 @@ jobs: - name: Install test requirements run: pip3 install -r requirements-test.txt + - name: Install the crud module for testing purposes + run: | + curl -L https://tarantool.io/release/2/installer.sh | bash + sudo apt install -y tt + tt rocks install crud + - name: Run tests run: make test-pure-install @@ -331,6 +337,12 @@ jobs: - name: Install test requirements run: pip3 install -r requirements-test.txt + - name: Install the crud module for testing purposes + run: | + curl -L https://tarantool.io/release/2/installer.sh | bash + sudo apt install -y tt + tt rocks install crud + - name: Run tests run: make test-pure-install @@ -492,6 +504,12 @@ jobs: - name: Install test requirements run: pip3 install -r requirements-test.txt + - name: Install the crud module for testing purposes + run: | + curl -L https://tarantool.io/release/2/installer.sh | bash + sudo apt install -y tt + tt rocks install crud + - name: Run tests run: make test-pure-install diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index cc64e383..1e66af13 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -39,4 +39,10 @@ jobs: - name: Install test requirements run: pip3 install -r requirements-test.txt + - name: Install the crud module for testing purposes + run: | + curl -L https://tarantool.io/release/2/installer.sh | bash + sudo apt install -y tt + tt rocks install crud + - run: make test diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 8a39c2f0..54f8daec 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -85,6 +85,12 @@ jobs: - name: Install test requirements run: pip3 install -r requirements-test.txt + - name: Install the crud module for testing purposes + run: | + curl -L https://tarantool.io/release/2/installer.sh | bash + sudo apt install -y tt + tt rocks install crud + - name: Run tests run: make test @@ -143,6 +149,15 @@ jobs: - name: Install test requirements run: pip3 install -r requirements-test.txt + - name: Install the crud module for testing purposes + # This is a workaround with TARANTOOL_DIR and should be reworked later. + # See more here: https://github.com/tarantool/tt/issues/282 + run: | + source tarantool-enterprise/env.sh + curl -L https://tarantool.io/release/2/installer.sh | bash + sudo apt install -y tt + tt rocks install crud TARANTOOL_DIR=$PWD/tarantool-enterprise + - name: Run tests run: | source tarantool-enterprise/env.sh @@ -196,6 +211,12 @@ jobs: - name: Install test requirements run: pip3 install -r requirements-test.txt + - name: Install the crud module for testing purposes + run: | + curl -L https://tarantool.io/release/2/installer.sh | bash + sudo apt install -y tt + tt rocks install crud + - name: Run tests run: make test-pure-install diff --git a/CHANGELOG.md b/CHANGELOG.md index 0664bab4..7e15a1e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support custom packer and unpacker factories (#191). +- Support [crud module](https://github.com/tarantool/crud) native API (#205). + ### Changed ### Fixed diff --git a/docs/source/api/submodule-crud.rst b/docs/source/api/submodule-crud.rst new file mode 100644 index 00000000..47852016 --- /dev/null +++ b/docs/source/api/submodule-crud.rst @@ -0,0 +1,4 @@ +module :py:mod:`tarantool.crud` +================================ + +.. automodule:: tarantool.crud diff --git a/docs/source/index.rst b/docs/source/index.rst index a8b7cc38..b2d31cd7 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -63,6 +63,7 @@ API Reference api/module-tarantool.rst api/submodule-connection.rst api/submodule-connection-pool.rst + api/submodule-crud.rst api/submodule-dbapi.rst api/submodule-error.rst api/submodule-mesh-connection.rst diff --git a/docs/source/quick-start.rst b/docs/source/quick-start.rst index 5fe64ff8..27633012 100644 --- a/docs/source/quick-start.rst +++ b/docs/source/quick-start.rst @@ -347,3 +347,123 @@ with out-of-band message processing: print(callback_res) >>> [[[100, 1]], [[200, 1]], [[300, 1]]] + + +Interaction with the crud module +---------------------------------- + +Through the :class:`~tarantool.Connection` object, you can access +`crud module `_ methods: + +.. code-block:: python + + >>> import tarantool + >>> from tarantool.error import CrudModuleError, CrudModuleManyError, DatabaseError + >>> conn = tarantool.Connection(host='localhost',port=3301) + + >>> conn.crud_ + conn.crud_count( conn.crud_insert( conn.crud_insert_object_many( + conn.crud_min( conn.crud_replace_object( conn.crud_stats( + conn.crud_unflatten_rows( conn.crud_upsert_many( conn.crud_delete( + conn.crud_insert_many( conn.crud_len( conn.crud_replace( + conn.crud_replace_object_many( conn.crud_storage_info( conn.crud_update( + conn.crud_upsert_object( conn.crud_get( conn.crud_insert_object( + conn.crud_max( conn.crud_replace_many( conn.crud_select( + conn.crud_truncate( conn.crud_upsert( conn.crud_upsert_object_many( + +As an example, consider :meth:`~tarantool.Connection.crud_insert` and :meth:`~tarantool.Connection.crud_insert_object_many`. +It is recommended to enclose calls in the try-except construction as follows: + +.. code-block:: python + + # Insert without exception: + >>> res = conn.crud_insert('tester', (3500,300,'Rob')) + >>> res + + >>> res. + res.metadata res.rows + >>> res.rows + [[3500, 300, 'Rob']] + >>> res.metadata + [{'name': 'id', 'type': 'unsigned'}, {'name': 'bucket_id', 'type': 'unsigned'}, {'name': 'name', 'type': 'string'}] + + # Insert with exception (duplicate key exists): + >>> try: + ... res = conn.crud_insert('tester', (3500,300,'Rob')) + ... except CrudModuleError as e: + ... exc_crud = e + ... + >>> exc_crud + CrudModuleError(0, 'Failed to insert: Duplicate key exists in unique index "primary_index" in space "tester" with old tuple - [3500, 300, "Rob"] and new tuple - [3500, 300, "Rob"]') + >>> exc_crud.extra_info_error + + >>> exc_crud.extra_info_error. + exc_crud.extra_info_error.class_name exc_crud.extra_info_error.err exc_crud.extra_info_error.file exc_crud.extra_info_error.line exc_crud.extra_info_error.str + >>> exc_crud.extra_info_error.class_name + 'InsertError' + >>> exc_crud.extra_info_error.str + 'InsertError: Failed to insert: Duplicate key exists in unique index "primary_index" in space "tester" with old tuple - [3500, 300, "Rob"] and new tuple - [3500, 300, "Rob"]' + + # In case of batch operation (*_many), CrudModuleManyError exception contains both result and errors (if there is a problem with at least one row). + >>> try: + ... res = conn.crud_insert_object_many('tester', ({'id':3,'bucket_id':100,'name':'Ann'}, {'id':4,'bucket_id':100,'name':'Sam'}), {'timeout':100, 'rollback_on_error':False}) + ... except CrudModuleManyError as e: + ... exc_crud = e + ... + >>> exc_crud + CrudModuleManyError(0, 'Got multiple errors, see errors_list') + >>> exc_crud.success_list # some of the rows were inserted. + + >>> exc_crud.success_list.rows + [[1, 100, 'Bob'], [2, 100, 'Rob']] + >>> exc_crud.errors_list # some of the rows were not inserted. + [, ] + >>> exc_crud.errors_list[0].str + 'CallError: Failed for 037adb3a-b9e3-4f78-a6d1-9f0cdb6cbefc: Function returned an error: Duplicate key exists in unique index "primary_index" in space "tester" with old tuple - [3500, 300, "Rob"] and new tuple - [3500, 100, "Mike"]' + >>> exc_crud.errors_list[1].str + 'InsertManyError: Failed to flatten object: FlattenError: Object is specified in bad format: FlattenError: Unknown field "second_name" is specified' + + # If there are no problems with any rows, the entire response will be contained in the res variable. + >>> res = conn.crud_insert_object_many('tester', ({'id':3,'bucket_id':100,'name':'Ann'}, {'id':4,'bucket_id':100,'name':'Sam'}), {'timeout':100, 'rollback_on_error':False}) + >>> res.rows + [[3, 100, 'Ann'], [4, 100, 'Sam']] + +If module crud not found on the router or user has not sufficient grants: + +.. code-block:: python + + >>> try: + ... res = conn.crud_insert('tester', (22221,300,'Rob')) + ... except DatabaseError as e: + ... exc_db = e + ... + >>> exc_db + DatabaseError(33, "Procedure 'crud.insert' is not defined. Ensure that you're calling crud.router and user has sufficient grants") + >>> exc_db.extra_info + BoxError(type='ClientError', file='/tmp/tarantool-20221003-6335-edruh3/tarantool-2.10.3/src/box/lua/call.c', line=112, message="Procedure 'crud.insert' is not defined", errno=0, errcode=33, fields=None, prev=None) + +Using :meth:`~tarantool.Connection.crud_select` and :meth:`~tarantool.Connection.crud_unflatten_rows`: + +.. code-block:: python + + >>> res = conn.crud_select('tester', [], {'first':2}) + >>> res + + >>> res.rows + [[1, 100, 'Mike'], [2, 100, 'Bill']] + >>> res.metadata + [{'name': 'id', 'type': 'unsigned'}, {'name': 'bucket_id', 'type': 'unsigned'}, {'name': 'name', 'type': 'string'}] + >>> r = conn.crud_unflatten_rows(res.rows, res.metadata) + >>> r + [{'id': 1, 'bucket_id': 100, 'name': 'Mike'}, {'id': 2, 'bucket_id': 100, 'name': 'Bill'}] + +Using :meth:`~tarantool.Connection.crud_truncate` and :meth:`~tarantool.Connection.crud_len`: + +.. code-block:: python + + >>> res = conn.crud_len('tester') + >>> res + 26 + >>> res = conn.crud_truncate('tester') + >>> res + True diff --git a/tarantool/connection.py b/tarantool/connection.py index fcaa7e83..ba23ea74 100644 --- a/tarantool/connection.py +++ b/tarantool/connection.py @@ -84,6 +84,8 @@ InternalError, ProgrammingError, NotSupportedError, + CrudModuleError, + CrudModuleManyError, SchemaReloadException, Warning, warn @@ -95,6 +97,12 @@ wrap_key, ENCODING_DEFAULT, ) +from tarantool.crud import ( + CrudResult, + CrudError, + call_crud, +) +from typing import Union # Based on https://realpython.com/python-interface/ class ConnectionInterface(metaclass=abc.ABCMeta): @@ -244,6 +252,198 @@ def execute(self, query, params): raise NotImplementedError + @abc.abstractmethod + def crud_insert(self, space_name, values, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_insert`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_insert_object(self, space_name, values, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_insert_object`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_insert_many(self, space_name, values, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_insert_many`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_insert_object_many(self, space_name, values, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_insert_object_many`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_get(self, space_name, key, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_get`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_update(self, space_name, key, operations=[], opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_update`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_delete(self, space_name, key, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_delete`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_replace(self, space_name, values, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_replace`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_replace_object(self, space_name, values, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_replace_object`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_replace_many(self, space_name, values, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_replace_many`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_replace_object_many(self, space_name, values, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_replace_object_many`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_upsert(self, space_name, values, operations=[], opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_upsert`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_upsert_object(self, space_name, values, operations=[], opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_upsert_object`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_upsert_many(self, space_name, values_operation, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_upsert_many`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_upsert_object_many(self, space_name, values_operation, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_upsert_object_many`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_select(self, space_name: str, conditions=[], opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_select`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_min(self, space_name, index_name, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_min`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_max(self, space_name, index_name, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_max`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_truncate(self, space_name, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_truncate`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_len(self, space_name, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_len`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_storage_info(self, opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_storage_info`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_count(self, space_name, conditions=[], opts={}): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_count`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_stats(self, space_name=None): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_stats`. + """ + + raise NotImplementedError + + @abc.abstractmethod + def crud_unflatten_rows(self, rows, metadata): + """ + Reference implementation: :meth:`~tarantool.Connection.crud_unflatten_rows`. + """ + + raise NotImplementedError + class Connection(ConnectionInterface): """ @@ -346,6 +546,13 @@ class Connection(ConnectionInterface): :value: :exc:`~tarantool.error.NotSupportedError` """ + CrudModuleError = CrudModuleError + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.CrudModuleError` + """ + def __init__(self, host, port, user=None, password=None, @@ -1797,3 +2004,774 @@ def _packer_factory(self): def _unpacker_factory(self): return self._unpacker_factory_impl(self) + + def crud_insert(self, space_name: str, values: Union[tuple, list], opts: dict={}) -> CrudResult: + """ + Inserts row through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param values: Tuple to be inserted. + :type values: :obj:`tuple` or :obj:`list` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(values, (tuple, list)) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.insert", space_name, values, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return CrudResult(crud_resp[0]) + + def crud_insert_object(self, space_name: str, values: dict, opts: dict={}) -> CrudResult: + """ + Inserts object row through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param values: Tuple to be inserted. + :type values: :obj:`dict` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(values, dict) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.insert_object", space_name, values, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return CrudResult(crud_resp[0]) + + def crud_insert_many(self, space_name: str, values: Union[tuple, list], opts: dict={}) -> CrudResult: + """ + Inserts batch rows through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param values: Tuple to be inserted. + :type values: :obj:`tuple` or :obj:`list` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(values, (tuple, list)) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.insert_many", space_name, values, opts) + + res = None + if crud_resp[0] is not None: + res = CrudResult(crud_resp[0]) + + if crud_resp[1] is not None: + errs = list() + for err in crud_resp[1]: + errs.append(CrudError(err)) + raise CrudModuleManyError(res, errs) + + return res + + def crud_insert_object_many(self, space_name: str, values: Union[tuple, list], opts: dict={}) -> CrudResult: + """ + Inserts batch object rows through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param values: Tuple to be inserted. + :type values: :obj:`tuple` or :obj:`list` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(values, (tuple, list)) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.insert_object_many", space_name, values, opts) + + res = None + if crud_resp[0] is not None: + res = CrudResult(crud_resp[0]) + + if crud_resp[1] is not None: + errs = list() + for err in crud_resp[1]: + errs.append(CrudError(err)) + raise CrudModuleManyError(res, errs) + + return res + + def crud_get(self, space_name: str, key: int, opts: dict={}) -> CrudResult: + """ + Gets row through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param key: The primary key value. + :type key: :obj:`int` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.get", space_name, key, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return CrudResult(crud_resp[0]) + + def crud_update(self, space_name: str, key: int, operations: list=[], opts: dict={}) -> CrudResult: + """ + Updates row through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param key: The primary key value. + :type key: :obj:`int` + + :param operations: The update operations for the crud module. + :type operations: :obj:`list`, optional + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(operations, list) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.update", space_name, key, operations, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return CrudResult(crud_resp[0]) + + def crud_delete(self, space_name: str, key: int, opts: dict={}) -> CrudResult: + """ + Deletes row through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param key: The primary key value. + :type key: :obj:`int` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.delete", space_name, key, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return CrudResult(crud_resp[0]) + + def crud_replace(self, space_name: str, values: Union[tuple, list], opts: dict={}) -> CrudResult: + """ + Replaces row through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param values: Tuple to be inserted. + :type values: :obj:`tuple` or :obj:`list` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(values, (tuple, list)) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.replace", space_name, values, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return CrudResult(crud_resp[0]) + + def crud_replace_object(self, space_name: str, values: dict, opts: dict={}) -> CrudResult: + """ + Replaces object row through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param values: Tuple to be inserted. + :type values: :obj:`dict` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(values, dict) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.replace_object", space_name, values, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return CrudResult(crud_resp[0]) + + def crud_replace_many(self, space_name: str, values: Union[tuple, list], opts: dict={}) -> CrudResult: + """ + Replaces batch rows through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param values: Tuple to be inserted. + :type values: :obj:`tuple` or :obj:`list` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(values, (tuple, list)) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.replace_many", space_name, values, opts) + + res = None + if crud_resp[0] is not None: + res = CrudResult(crud_resp[0]) + + if crud_resp[1] is not None: + errs = list() + for err in crud_resp[1]: + errs.append(CrudError(err)) + raise CrudModuleManyError(res, errs) + + return res + + def crud_replace_object_many(self, space_name: str, values: Union[tuple, list], opts: dict={}) -> CrudResult: + """ + Replaces batch object rows through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param values: Tuple to be inserted. + :type values: :obj:`tuple` or :obj:`list` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(values, (tuple, list)) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.replace_object_many", space_name, values, opts) + + res = None + if crud_resp[0] is not None: + res = CrudResult(crud_resp[0]) + + if crud_resp[1] is not None: + errs = list() + for err in crud_resp[1]: + errs.append(CrudError(err)) + raise CrudModuleManyError(res, errs) + + return res + + def crud_upsert(self, space_name: str, values: Union[tuple, list], operations: list=[], opts: dict={}) -> CrudResult: + """ + Upserts row through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param values: Tuple to be inserted. + :type values: :obj:`tuple` or :obj:`list` + + :param operations: The update operations for the crud module. + :type operations: :obj:`list`, optional + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(values, (tuple, list)) + assert isinstance(operations, list) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.upsert", space_name, values, operations, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return CrudResult(crud_resp[0]) + + def crud_upsert_object(self, space_name: str, values: dict, operations: list=[], opts: dict={}) -> CrudResult: + """ + Upserts object row through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param values: Tuple to be inserted. + :type values: :obj:`dict` + + :param operations: The update operations for the crud module. + :type operations: :obj:`list`, optional + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(values, dict) + assert isinstance(operations, list) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.upsert_object", space_name, values, operations, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return CrudResult(crud_resp[0]) + + def crud_upsert_many(self, space_name: str, values_operation: Union[tuple, list], opts: dict={}) -> CrudResult: + """ + Upserts batch rows through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param values_operation: The data with update operations. + :type values_operation: :obj:`tuple` or :obj:`list` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(values_operation, (tuple, list)) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.upsert_many", space_name, values_operation, opts) + + res = None + if crud_resp[0] is not None: + res = CrudResult(crud_resp[0]) + + if crud_resp[1] is not None: + errs = list() + for err in crud_resp[1]: + errs.append(CrudError(err)) + raise CrudModuleManyError(res, errs) + + return res + + def crud_upsert_object_many(self, space_name: str, values_operation: Union[tuple, list], opts: dict={}) -> CrudResult: + """ + Upserts batch object rows through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param values_operation: The data with update operations. + :type values_operation: :obj:`tuple` or :obj:`list` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(values_operation, (tuple, list)) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.upsert_object_many", space_name, values_operation, opts) + + res = None + if crud_resp[0] is not None: + res = CrudResult(crud_resp[0]) + + if crud_resp[1] is not None: + errs = list() + for err in crud_resp[1]: + errs.append(CrudError(err)) + raise CrudModuleManyError(res, errs) + + return res + + def crud_select(self, space_name: str, conditions: list=[], opts: dict={}) -> CrudResult: + """ + Selects rows through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param conditions: The select conditions for the crud module. + :type conditions: :obj:`list`, optional + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError`select + """ + + assert isinstance(space_name, str) + assert isinstance(conditions, (tuple, list)) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.select", space_name, conditions, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return CrudResult(crud_resp[0]) + + def crud_min(self, space_name: str, index_name: str, opts: dict={}) -> CrudResult: + """ + Gets rows with minimum value in the specified index through + the `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param index_name: The name of the index. + :type index_name: :obj:`str` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.min", space_name, index_name, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return CrudResult(crud_resp[0]) + + def crud_max(self, space_name: str, index_name: str, opts: dict={}) -> CrudResult: + """ + Gets rows with maximum value in the specified index through + the `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param index_name: The name of the index. + :type index_name: :obj:`str` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.max", space_name, index_name, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return CrudResult(crud_resp[0]) + + def crud_truncate(self, space_name: str, opts: dict={}) -> bool: + """ + Truncate rows through + the `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :obj:`bool` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.truncate", space_name, opts) + + # In absence of an error, crud does not give + # variable err as nil (as in most cases). + if len(crud_resp) != 1: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return crud_resp[0] + + def crud_len(self, space_name: str, opts: dict={}) -> int: + """ + Gets the number of tuples in the space through + the `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :obj:`int` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.len", space_name, opts) + + # In absence of an error, crud does not give + # variable err as nil (as in most cases). + if len(crud_resp) != 1: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return crud_resp[0] + + def crud_storage_info(self, opts: dict={}) -> dict: + """ + Gets storages status through the + `crud `__. + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :obj:`dict` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.storage_info", opts) + + # In absence of an error, crud does not give + # variable err as nil (as in most cases). + if len(crud_resp) != 1: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return crud_resp[0] + + def crud_count(self, space_name: str, conditions: list=[], opts: dict={}) -> int: + """ + Gets rows count through the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str` + + :param conditions: The conditions for the crud module. + :type conditions: :obj:`list`, optional + + :param opts: The opts for the crud module. + :type opts: :obj:`dict`, optional + + :rtype: :obj:`int` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + assert isinstance(space_name, str) + assert isinstance(conditions, (tuple, list)) + assert isinstance(opts, dict) + + crud_resp = call_crud(self, "crud.count", space_name, conditions, opts) + + if crud_resp[1] is not None: + raise CrudModuleError(None, CrudError(crud_resp[1])) + + return crud_resp[0] + + def crud_stats(self, space_name: str=None) -> CrudResult: + """ + Gets statistics from the + `crud `__. + + :param space_name: The name of the target space. + :type space_name: :obj:`str`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.DatabaseError` + """ + + if space_name is not None: + assert isinstance(space_name, str) + + crud_resp = call_crud(self, "crud.stats", space_name) + + res = None + if len(crud_resp.data[0]) > 0: + res = CrudResult(crud_resp.data[0]) + + return res + + def crud_unflatten_rows(self, rows: list, metadata: list) -> list: + """ + Makes rows unflatten through the + `crud `__. + + :param rows: The rows to unflatten. + :type rows: :obj:`list` + + :param metadata: The metadata to unflatten. + :type metadata: :obj:`list` + + :rtype: :obj:`list` + """ + + assert isinstance(rows, (tuple, list)) + assert isinstance(metadata, (tuple, list)) + + res = [] + for row in rows: + row_res = {} + for field_idx, field in enumerate(row): + row_res[metadata[field_idx]['name']] = field + res.append(row_res) + + return res diff --git a/tarantool/connection_pool.py b/tarantool/connection_pool.py index 10523c84..7b3ac250 100644 --- a/tarantool/connection_pool.py +++ b/tarantool/connection_pool.py @@ -1025,3 +1025,638 @@ def execute(self, query, params=None, *, mode=None): raise ValueError("Please, specify 'mode' keyword argument") return self._send(mode, 'execute', query, params) + + def crud_insert(self, space_name, values, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_insert request on the pool server: + inserts row through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_insert`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_insert.params.space_name`. + + :param values: Refer to + :paramref:`~tarantool.Connection.crud_insert.params.values`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_insert.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_insert', space_name, values, opts) + + def crud_insert_object(self, space_name, values, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_insert_object request on the pool server: + inserts object row through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_insert_object`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_insert_object.params.space_name`. + + :param values: Refer to + :paramref:`~tarantool.Connection.crud_insert_object.params.values`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_insert_object.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_insert_object', space_name, values, opts) + + def crud_insert_many(self, space_name, values, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_insert_many request on the pool server: + inserts batch rows through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_insert_many`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_insert_many.params.space_name`. + + :param values: Refer to + :paramref:`~tarantool.Connection.crud_insert_many.params.values`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_insert_many.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_insert_many', space_name, values, opts) + + def crud_insert_object_many(self, space_name, values, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_insert_object_many request on the pool server: + inserts batch object rows through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_insert_object_many`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_insert_object_many.params.space_name`. + + :param values: Refer to + :paramref:`~tarantool.Connection.crud_insert_object_many.params.values`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_insert_object_many.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_insert_object_many', space_name, values, opts) + + def crud_get(self, space_name, key, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_get request on the pool server: + gets row through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_get`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_get.params.space_name`. + + :param key: Refer to + :paramref:`~tarantool.Connection.crud_get.params.key`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_get.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_get', space_name, key, opts) + + def crud_update(self, space_name, key, operations=[], opts={}, *, mode=Mode.ANY): + """ + Execute an crud_update request on the pool server: + updates row through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_update`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_update.params.space_name`. + + :param key: Refer to + :paramref:`~tarantool.Connection.crud_update.params.key`. + + :param operations: Refer to + :paramref:`~tarantool.Connection.crud_update.params.operations`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_update.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_update', space_name, key, operations, opts) + + def crud_delete(self, space_name, key, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_delete request on the pool server: + deletes row through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_delete`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_delete.params.space_name`. + + :param key: Refer to + :paramref:`~tarantool.Connection.crud_delete.params.key`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_delete.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_delete', space_name, key, opts) + + def crud_replace(self, space_name, values, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_replace request on the pool server: + replaces row through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_replace`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_replace.params.space_name`. + + :param values: Refer to + :paramref:`~tarantool.Connection.crud_replace.params.values`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_replace.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_replace', space_name, values, opts) + + def crud_replace_object(self, space_name, values, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_replace_object request on the pool server: + replaces object row through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_replace_object`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_replace_object.params.space_name`. + + :param values: Refer to + :paramref:`~tarantool.Connection.crud_replace_object.params.values`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_replace_object.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_replace_object', space_name, values, opts) + + def crud_replace_many(self, space_name, values, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_replace_many request on the pool server: + replaces batch rows through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_replace_many`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_replace_many.params.space_name`. + + :param values: Refer to + :paramref:`~tarantool.Connection.crud_replace_many.params.values`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_replace_many.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_replace_many', space_name, values, opts) + + def crud_replace_object_many(self, space_name, values, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_replace_object_many request on the pool server: + replaces batch object rows through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_replace_object_many`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_replace_object_many.params.space_name`. + + :param values: Refer to + :paramref:`~tarantool.Connection.crud_replace_object_many.params.values`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_replace_object_many.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_replace_object_many', space_name, values, opts) + + def crud_upsert(self, space_name, values, operations=[], opts={}, *, mode=Mode.ANY): + """ + Execute an crud_upsert request on the pool server: + upserts row through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_upsert`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_upsert.params.space_name`. + + :param values: Refer to + :paramref:`~tarantool.Connection.crud_upsert.params.values`. + + :param operations: Refer to + :paramref:`~tarantool.Connection.crud_upsert.params.operations`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_upsert.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_upsert', space_name, values, operations, opts) + + def crud_upsert_object(self, space_name, values, operations=[], opts={}, *, mode=Mode.ANY): + """ + Execute an crud_upsert_object request on the pool server: + upserts object row through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_upsert_object`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_upsert_object.params.space_name`. + + :param values: Refer to + :paramref:`~tarantool.Connection.crud_upsert_object.params.values`. + + :param operations: Refer to + :paramref:`~tarantool.Connection.crud_upsert_object.params.operations`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_upsert_object.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_upsert_object', space_name, values, operations, opts) + + def crud_upsert_many(self, space_name, values_operation, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_upsert_many request on the pool server: + upserts batch rows through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_upsert_many`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_upsert_many.params.space_name`. + + :param values_operation: Refer to + :paramref:`~tarantool.Connection.crud_upsert_many.params.values_operation`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_upsert_many.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_upsert_many', space_name, values_operation, opts) + + def crud_upsert_object_many(self, space_name, values_operation, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_upsert_object_many request on the pool server: + upserts batch object rows through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_upsert_object_many`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_upsert_object_many.params.space_name`. + + :param values_operation: Refer to + :paramref:`~tarantool.Connection.crud_upsert_object_many.params.values_operation`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_upsert_object_many.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_upsert_object_many', space_name, values_operation, opts) + + def crud_select(self, space_name, conditions=[], opts={}, *, mode=Mode.ANY): + """ + Execute an crud_select request on the pool server: + selects rows through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_select`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_select.params.space_name`. + + :param conditions: Refer to + :paramref:`~tarantool.Connection.crud_select.params.conditions`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_select.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_select', space_name, conditions, opts) + + def crud_min(self, space_name, index_name, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_min request on the pool server: + gets rows with minimum value in the specified index through + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_min`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_min.params.space_name`. + + :param index_name: Refer to + :paramref:`~tarantool.Connection.crud_min.params.index_name`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_min.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_min', space_name, index_name, opts) + + def crud_max(self, space_name, index_name, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_max request on the pool server: + gets rows with maximum value in the specified index through + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_max`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_max.params.space_name`. + + :param index_name: Refer to + :paramref:`~tarantool.Connection.crud_max.params.index_name`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_max.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_max', space_name, index_name, opts) + + def crud_len(self, space_name, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_len request on the pool server: + gets the number of tuples in the space through + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_len`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_len.params.space_name`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_len.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_len', space_name, opts) + + def crud_storage_info(self, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_storage_info request on the pool server: + gets storages status through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_storage_info`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_storage_info.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_storage_info', opts) + + def crud_count(self, space_name, conditions=[], opts={}, *, mode=Mode.ANY): + """ + Execute an crud_count request on the pool server: + gets rows count through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_count`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_count.params.space_name`. + + :param conditions: Refer to + :paramref:`~tarantool.Connection.crud_count.params.conditions`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_count.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_count', space_name, conditions, opts) + + def crud_stats(self, space_name=None, *, mode=Mode.ANY): + """ + Execute an crud_stats request on the pool server: + gets statistics through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_stats`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_stats.params.space_name`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_stats', space_name) + + def crud_unflatten_rows(self, rows, metadata, *, mode=Mode.ANY): + """ + Makes rows unflatten through the + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_unflatten_rows`. + + :param rows: Refer to + :paramref:`~tarantool.Connection.crud_unflatten_rows.params.rows`. + + :param metadata: Refer to + :paramref:`~tarantool.Connection.crud_unflatten_rows.params.metadata`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_unflatten_rows', rows, metadata) + + def crud_truncate(self, space_name, opts={}, *, mode=Mode.ANY): + """ + Execute an crud_truncate request on the pool server: + truncates rows through + `crud `__. + Refer to :meth:`~tarantool.Connection.crud_truncate`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.crud_truncate.params.space_name`. + + :param opts: Refer to + :paramref:`~tarantool.Connection.crud_truncate.params.opts`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.crud.CrudResult` + + :raise: :exc:`~tarantool.error.CrudModuleError`, + :exc:`~tarantool.error.DatabaseError` + """ + + return self._send(mode, 'crud_truncate', space_name, opts) diff --git a/tarantool/crud.py b/tarantool/crud.py new file mode 100644 index 00000000..dee1be36 --- /dev/null +++ b/tarantool/crud.py @@ -0,0 +1,72 @@ +""" +This module provides API for interaction with the `crud`_ module. + +.. _crud: https://github.com/tarantool/crud/ +""" + +from tarantool.error import DatabaseError, ER_NO_SUCH_PROC, ER_ACCESS_DENIED + + +class CrudResponse(object): + """ + Contains response fields from the `crud`_ module that correspond + to the Lua implementation. + + .. _crud: https://github.com/tarantool/crud/ + """ + + def __init__(self, response): + """ + Sets response fields as in Lua implementation. + + :param response: The response object of the crud module call. + :type response: :class:`~tarantool.response.Response` + """ + + if isinstance(response, dict): + for response_field_name in response.keys(): + if isinstance(response_field_name, bytes): + setattr(self, response_field_name.decode(), response[response_field_name]) + else: + setattr(self, response_field_name, response[response_field_name]) + else: + raise RuntimeError('Unable to decode response to object due to unknown type') + + +class CrudResult(CrudResponse): + """ + Contains result's fields from result variable + of crud module operation. + """ + + +class CrudError(CrudResponse): + """ + Contains error's fields from error variable + of crud module operation. + """ + + +def call_crud(conn, *args): + """ + Calls the crud via connection.call with try/except block. + + :param conn: The connection object for the crud module call. + :type conn: :class:`~tarantool.connection.Connection` + + :param args: The method name and args for the crud method. + :type args: :obj:`tuple` + + :raise: :exc:`~tarantool.error.DatabaseError` + + :meta private: + """ + + try: + crud_resp = conn.call(*args) + except DatabaseError as e: + if e.code == ER_NO_SUCH_PROC or e.code == ER_ACCESS_DENIED: + exc_msg = ". Ensure that you're calling crud.router and user has sufficient grants" + raise DatabaseError(e.code, e.message + exc_msg, extra_info=e.extra_info) from e + + return crud_resp diff --git a/tarantool/error.py b/tarantool/error.py index c8737b49..29c2430f 100644 --- a/tarantool/error.py +++ b/tarantool/error.py @@ -330,6 +330,51 @@ class PoolTolopogyError(DatabaseError): pass +class CrudModuleError(DatabaseError): + """ + Exception raised for errors that are related to + the operation result of `crud`_ module. + + .. _crud: https://github.com/tarantool/crud + """ + + def __init__(self, _, error): + """ + Sets fields with result and errors. + + :param args: The tuple from the crud module with result and errors. + :type args: :obj:`tuple` + """ + + super(CrudModuleError, self).__init__(0, error.err) + # Sets tarantool.crud.CrudError object. + self.extra_info_error = error + + +class CrudModuleManyError(DatabaseError): + """ + Exception raised for errors that are related to + the batching operation result of `crud`_ module. + + .. _crud: https://github.com/tarantool/crud + """ + + def __init__(self, success, error): + """ + Sets fields with result and errors. + + :param args: The tuple from the crud module with result and errors. + :type args: :obj:`tuple` + """ + + exc_msg = "Got multiple errors, see errors_list and success_list" + super(CrudModuleManyError, self).__init__(0, exc_msg) + # Sets list of tarantool.crud.CrudResult objects. + self.success_list = success + # Sets list of tarantool.crud.CrudError objects. + self.errors_list = error + + # always print this warnings warnings.filterwarnings("always", category=NetworkWarning) @@ -528,6 +573,12 @@ def warn(message, warning_class): } +# Response error code for case "Requested procedure is not defined". +ER_NO_SUCH_PROC = 33 +# Response error code for case "Access is denied for user". +ER_ACCESS_DENIED = 42 + + def tnt_strerror(num): """ Parse Tarantool error to string data. diff --git a/test/suites/__init__.py b/test/suites/__init__.py index 49014e97..c8fa35d7 100644 --- a/test/suites/__init__.py +++ b/test/suites/__init__.py @@ -23,6 +23,7 @@ from .test_error_ext import TestSuite_ErrorExt from .test_push import TestSuite_Push from .test_connection import TestSuite_Connection +from .test_crud import TestSuite_Crud test_cases = (TestSuite_Schema_UnicodeConnection, TestSuite_Schema_BinaryConnection, @@ -31,7 +32,7 @@ TestSuite_Encoding, TestSuite_Pool, TestSuite_Ssl, TestSuite_Decimal, TestSuite_UUID, TestSuite_Datetime, TestSuite_Interval, TestSuite_ErrorExt, TestSuite_Push, - TestSuite_Connection,) + TestSuite_Connection, TestSuite_Crud,) def load_tests(loader, tests, pattern): suite = unittest.TestSuite() diff --git a/test/suites/crud_server.lua b/test/suites/crud_server.lua new file mode 100644 index 00000000..ba6fe82a --- /dev/null +++ b/test/suites/crud_server.lua @@ -0,0 +1,68 @@ +#!/usr/bin/env tarantool + +local crud = require('crud') +local vshard = require('vshard') + +local admin_listen = os.getenv("ADMIN") +local primary_listen = os.getenv("LISTEN") + +require('console').listen(admin_listen) +box.cfg{ + listen = primary_listen, + memtx_memory = 0.1 * 1024^3, -- 0.1 GiB + pid_file = "box.pid", +} + +box.schema.user.grant( + 'guest', + 'read,write,execute', + 'universe' +) +box.schema.create_space( + 'tester', { + format = { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + } +}) +box.space.tester:create_index('primary_index', { + parts = { + {field = 1, type = 'unsigned'}, + }, +}) +box.space.tester:create_index('bucket_id', { + parts = { + {field = 2, type = 'unsigned'}, + }, + unique = false, +}) + +-- Setup vshard. +_G.vshard = vshard +box.once('guest', function() + box.schema.user.grant('guest', 'super') +end) +local uri = 'guest@0.0.0.0:' .. primary_listen +local cfg = { + bucket_count = 300, + sharding = { + [box.info().cluster.uuid] = { + replicas = { + [box.info().uuid] = { + uri = uri, + name = 'storage', + master = true, + }, + }, + }, + }, +} +vshard.storage.cfg(cfg, box.info().uuid) +vshard.router.cfg(cfg) +vshard.router.bootstrap() + +-- Initialize crud. +crud.init_storage() +crud.init_router() +crud.cfg{stats = true} diff --git a/test/suites/test_crud.py b/test/suites/test_crud.py new file mode 100644 index 00000000..633b7159 --- /dev/null +++ b/test/suites/test_crud.py @@ -0,0 +1,751 @@ +import re +import sys +import time +import unittest +import tarantool +from .lib.tarantool_server import TarantoolServer +from tarantool.error import DatabaseError + + +def create_server(): + srv = TarantoolServer() + srv.script = 'test/suites/crud_server.lua' + srv.start() + + return srv + + +@unittest.skipIf(sys.platform.startswith("win"), + "Crud tests on windows platform are not supported: " + + "complexity of the vshard replicaset configuration") +class TestSuite_Crud(unittest.TestCase): + + @classmethod + def setUpClass(self): + print(' CRUD '.center(70, '='), file=sys.stderr) + print('-' * 70, file=sys.stderr) + # Create server and extract helpful fields for tests. + self.srv = create_server() + self.host = self.srv.host + self.port = self.srv.args['primary'] + + def setUp(self): + time.sleep(1) + # Open connections to instance. + self.conn = tarantool.Connection(host=self.host, port=self.port, + user='guest', password='') + self.conn_mesh = tarantool.MeshConnection(host=self.host, port=self.port, + user='guest', password='') + self.conn_pool = tarantool.ConnectionPool([{'host':self.host, 'port':self.port}], + user='guest', password='') + # Time for vshard group configuration. + time.sleep(1) + + crud_test_cases = { + 'crud_insert': { + 'success': { + 'input': { + 'args': ['tester', [1, 100, 'Mike'], {'timeout': 10}], + }, + 'output': { + 'rows': [[1, 100, 'Mike']], + }, + }, + 'error': { + 'input': { + 'args': ['tester', [1, 100, 'Bob'], {'timeout': 10}], + }, + 'output': { + 'str': [ + r'Duplicate key exists', + ], + }, + }, + }, + 'crud_insert_object': { + 'success': { + 'input': { + 'args': ['tester', {'id': 2, 'bucket_id': 100, 'name': 'Ann'}, {'timeout': 10}], + }, + 'output': { + 'rows': [[2, 100, 'Ann']], + }, + }, + 'error': { + 'input': { + 'args': ['tester', {'id': 2, 'bucket_id': 100, 'name': 'Logan'}, {'timeout': 10}], + }, + 'output': { + 'str': [ + r'Duplicate key exists', + ], + }, + }, + }, + 'crud_insert_many': { + 'success': { + 'input': { + 'args': [ + 'tester', + [ + [3, 100, 'Jacob'], + [4, 100, 'Wyatt'], + [5, 100, 'David'], + [6, 100, 'Leo'], + ], + {'timeout': 10}, + ], + }, + 'output': { + 'rows': [ + [3, 100, 'Jacob'], + [4, 100, 'Wyatt'], + [5, 100, 'David'], + [6, 100, 'Leo'], + ], + }, + }, + 'error': { + 'input': { + 'args': [ + 'tester', + [ + [3, 100, 'Julian'], + [4, 100, 'Hudson'], + [7, 100, 'Grayson'], + [8, 100, 'Ezra'], + ], + {'timeout': 10}, + ], + }, + 'output': { + 'str': [ + r'Duplicate key exists', + ], + 'res_rows': [[7, 100, 'Grayson'], [8, 100, 'Ezra']] + }, + }, + }, + 'crud_insert_object_many': { + 'success': { + 'input': { + 'args': [ + 'tester', + [ + {'id': 9, 'bucket_id': 100, 'name': 'Sharar'}, + {'id': 10, 'bucket_id': 100, 'name': 'Thaddeus'}, + {'id': 11, 'bucket_id': 100, 'name': 'Tobit'}, + {'id': 12, 'bucket_id': 100, 'name': 'Zeb'}, + ], + {'timeout': 10}, + ], + }, + 'output': { + 'rows': [ + [9, 100, 'Sharar'], + [10, 100, 'Thaddeus'], + [11, 100, 'Tobit'], + [12, 100, 'Zeb'], + ], + }, + }, + 'error': { + 'input': { + 'args': [ + 'tester', + [ + {'id': 9, 'bucket_id': 100, 'name': 'Silvanus'}, + {'id': 10, 'bucket_id': 100, 'name': 'Timeus'}, + {'id': 13, 'bucket_id': 100, 'name': 'Uzzi'}, + {'id': 14, 'bucket_id': 100, 'name': 'Zimiri'}, + ], + {'timeout': 10}, + ], + }, + 'output': { + 'str': [ + r'Duplicate key exists', + ], + 'res_rows': [[13, 100, 'Uzzi'], [14, 100, 'Zimiri']] + }, + }, + }, + 'crud_get': { + 'success': { + 'input': { + 'args': ['tester', 1, {'timeout': 10}], + }, + 'output': { + 'rows': [[1, 100, 'Mike']], + }, + }, + 'error': { + 'input': { + 'args': ['no-such-space-name', [1, 100, 'Bob'], {'timeout': 10}], + }, + 'output': { + 'str': [ + r'GetError: Space "no-such-space-name" doesn\'t exist', + ], + }, + }, + }, + 'crud_update': { + 'success': { + 'input': { + 'args': ['tester', 1, [['+', 'bucket_id', 1]], {'timeout': 10}], + }, + 'output': { + 'rows': [[1, 101, 'Mike']], + }, + }, + 'error': { + 'input': { + 'args': ['tester', 1, [['+', 'age', 1]], {'timeout': 10}], + }, + 'output': { + 'str': [ + r"UpdateError", + ], + }, + }, + }, + 'crud_delete': { + 'success': { + 'input': { + 'args': ['tester', 1, {'timeout': 10}], + }, + 'output': { + 'rows': [[1, 101, 'Mike']], + }, + }, + 'error': { + 'input': { + 'args': ['no-such-space-name', 1, {'timeout': 10}], + }, + 'output': { + 'str': [ + r'DeleteError: Space "no-such-space-name" doesn\'t exist', + ], + }, + }, + }, + 'crud_replace': { + 'success': { + 'input': { + 'args': ['tester', [2, 100, 'Alice'], {'timeout': 10}], + }, + 'output': { + 'rows': [[2, 100, 'Alice']], + }, + }, + 'error': { + 'input': { + 'args': ['tester', [1, 100, 0], {'timeout': 10}], + }, + 'output': { + 'str': [ + r'expected string', + ], + }, + }, + }, + 'crud_replace_object': { + 'success': { + 'input': { + 'args': ['tester', {'id': 2, 'bucket_id': 100, 'name': 'Eliza'}, {'timeout': 10}], + }, + 'output': { + 'rows': [[2, 100, 'Eliza']], + }, + }, + 'error': { + 'input': { + 'args': ['tester', {'id': 2, 'bucket_id': 100, 'name': 0}, {'timeout': 10}], + }, + 'output': { + 'str': [ + r'expected string', + ], + }, + }, + }, + 'crud_replace_many': { + 'success': { + 'input': { + 'args': [ + 'tester', + [ + [2, 100, 'Cephus'], + [3, 100, 'Esau'], + [4, 100, 'Haman'], + [5, 100, 'Gershon'], + ], + {'timeout': 10}, + ], + }, + 'output': { + 'rows': [ + [2, 100, 'Cephus'], + [3, 100, 'Esau'], + [4, 100, 'Haman'], + [5, 100, 'Gershon'], + ], + }, + }, + 'error': { + 'input': { + 'args': [ + 'tester', + [ + [3, 100, 'Ephron'], + [4, 100, 'Ethan'], + [7, 100, 0], + [8, 100, 0], + ], + {'timeout': 10}, + ], + }, + 'output': { + 'str': [ + r'expected string', + ], + 'res_rows': [[3, 100, 'Ephron'], [4, 100, 'Ethan']] + }, + }, + }, + 'crud_replace_object_many': { + 'success': { + 'input': { + 'args': [ + 'tester', + [ + {'id': 2, 'bucket_id': 100, 'name': 'Cephus'}, + {'id': 3, 'bucket_id': 100, 'name': 'Esau'}, + {'id': 4, 'bucket_id': 100, 'name': 'Haman'}, + {'id': 5, 'bucket_id': 100, 'name': 'Gershon'}, + ], + {'timeout': 10}, + ], + }, + 'output': { + 'rows': [ + [2, 100, 'Cephus'], + [3, 100, 'Esau'], + [4, 100, 'Haman'], + [5, 100, 'Gershon'], + ], + }, + }, + 'error': { + 'input': { + 'args': [ + 'tester', + [ + {'id': 3, 'bucket_id': 100, 'name': 'Ephron'}, + {'id': 4, 'bucket_id': 100, 'name': 'Ethan'}, + {'id': 7, 'bucket_id': 100, 'name': 0}, + {'id': 8, 'bucket_id': 100, 'name': 0}, + ], + {'timeout': 10}, + ], + }, + 'output': { + 'str': [ + r'expected string', + ], + 'res_rows': [[3, 100, 'Ephron'], [4, 100, 'Ethan']] + }, + }, + }, + 'crud_upsert': { + 'success': { + 'input': { + 'args': ['tester', [2, 100, 'Cephus'], [['+', 'bucket_id', 1]], {'timeout': 10}], + }, + 'output': { + 'rows': [], + }, + }, + 'error': { + 'input': { + 'args': ['tester', [2, 100, 'Cephus'], [['+', 'age', 1]], {'timeout': 10}], + }, + 'output': { + 'str': [ + r"UpsertError", + ], + }, + }, + }, + 'crud_upsert_object': { + 'success': { + 'input': { + 'args': ['tester', {'id': 2, 'bucket_id': 100, 'name': 'Cephus'}, + [['+', 'bucket_id', 1]], {'timeout': 10}], + }, + 'output': { + 'rows': [], + }, + }, + 'error': { + 'input': { + 'args': ['tester', {'id': 2, 'bucket_id': 100, 'name': 'Cephus'}, + [['+', 'age', 1]], {'timeout': 10}], + }, + 'output': { + 'str': [ + r"UpsertError", + ], + }, + }, + }, + 'crud_upsert_many': { + 'success': { + 'input': { + 'args': [ + 'tester', + [ + [[2, 100, 'Cephus'], [['+', 'bucket_id', 1]]], + [[3, 100, 'Esau'], [['+', 'bucket_id', 1]]], + [[4, 100, 'Haman'], [['+', 'bucket_id', 1]]], + [[5, 100, 'Gershon'], [['+', 'bucket_id', 1]]], + ], + {'timeout': 10}, + ], + }, + 'output': {}, + }, + 'error': { + 'input': { + 'args': [ + 'tester', + [ + [[3, 100, 'Ephron'], [['+', 'bucket_id', 1]]], + [[4, 100, 'Ethan'], [['+', 'bucket_id', 1]]], + [[7, 100, 0], [['+', 'bucket_id', 1]]], + [[8, 100, 0], [['+', 'bucket_id', 1]]], + ], + {'timeout': 10}, + ], + }, + 'output': { + 'str': [ + r'expected string', + ], + }, + }, + }, + 'crud_upsert_object_many': { + 'success': { + 'input': { + 'args': [ + 'tester', + [ + [{'id': 2, 'bucket_id': 100, 'name': 'Cephus'}, [['+', 'bucket_id', 1]]], + [{'id': 3, 'bucket_id': 100, 'name': 'Esau'}, [['+', 'bucket_id', 1]]], + [{'id': 4, 'bucket_id': 100, 'name': 'Haman'}, [['+', 'bucket_id', 1]]], + [{'id': 5, 'bucket_id': 100, 'name': 'Gershon'}, [['+', 'bucket_id', 1]]], + ], + {'timeout': 10}, + ], + }, + 'output': {}, + }, + 'error': { + 'input': { + 'args': [ + 'tester', + [ + [{'id': 3, 'bucket_id': 100, 'name': 'Ephron'}, [['+', 'bucket_id', 1]]], + [{'id': 4, 'bucket_id': 100, 'name': 'Ethan'}, [['+', 'bucket_id', 1]]], + [{'id': 7, 'bucket_id': 100, 'name': 0}, [['+', 'bucket_id', 1]]], + [{'id': 8, 'bucket_id': 100, 'name': 0}, [['+', 'bucket_id', 1]]], + ], + {'timeout': 10}, + ], + }, + 'output': { + 'str': [ + r'expected string', + ], + }, + }, + }, + 'crud_select': { + 'success': { + 'input': { + 'args': ['tester', [], {'first': 3}], + }, + 'output': { + 'rows': [[2, 104, 'Cephus'], [3, 104, 'Ephron'], [4, 104, 'Ethan']], + }, + }, + 'error': { + 'input': { + 'args': ['no-such-space-name'], + }, + 'output': { + 'str': [ + r'SelectError: Space "no-such-space-name" doesn\'t exist', + ], + }, + }, + }, + 'crud_min': { + 'success': { + 'input': { + 'args': ['tester', 'bucket_id', {'timeout': 10}], + }, + 'output': { + 'rows': [[6, 100, 'Leo']], + }, + }, + 'error': { + 'input': { + 'args': ['tester', 'no-idx'], + }, + 'output': { + 'str': [ + r'BorderError: Index "no-idx" of space "tester" doesn\'t exist', + ], + }, + }, + }, + 'crud_max': { + 'success': { + 'input': { + 'args': ['tester', 'bucket_id', {'timeout': 10}], + }, + 'output': { + 'rows': [[4, 104, 'Ethan']], + }, + }, + 'error': { + 'input': { + 'args': ['tester', 'no-idx', {'timeout': 10}], + }, + 'output': { + 'str': [ + r'BorderError: Index "no-idx" of space "tester" doesn\'t exist', + ], + }, + }, + }, + 'crud_len': { + 'success': { + 'input': { + 'args': ['tester', {'timeout': 10}], + }, + 'output': { + 'scalar': 13, + }, + }, + 'error': { + 'input': { + 'args': ['no-such-space-name', {'timeout': 10}], + }, + 'output': { + 'str': [ + r'LenError: Space "no-such-space-name" doesn\'t exist', + ], + }, + }, + }, + 'crud_count': { + 'success': { + 'input': { + 'args': ['tester', [['==', 'bucket_id', 100]], {'timeout': 10}], + }, + 'output': { + 'scalar': 9, + }, + }, + 'error': { + 'input': { + 'args': ['no-such-space-name', [['==', 'bucket_id', 100]], {'timeout': 10}], + }, + 'output': { + 'str': [ + r'CountError: Space "no-such-space-name" doesn\'t exist', + ], + }, + }, + }, + 'crud_unflatten_rows': { + 'success': { + 'input': { + 'args': [ + [ + [1, 100, 'Mike'], + [2, 100, 'Mike'], + [3, 100, 'Mike'], + [4, 100, 'Mike'], + [5, 200, 'Bill'], + [6, 300, 'Rob'], + ], + [ + {'name': 'id', 'type': 'unsigned'}, + {'name': 'bucket_id', 'type': 'unsigned'}, + {'name': 'name', 'type': 'string'} + ], + ], + }, + 'output': { + 'scalar': [ + {'bucket_id': 100, 'name': 'Mike', 'id': 1}, + {'bucket_id': 100, 'name': 'Mike', 'id': 2}, + {'bucket_id': 100, 'name': 'Mike', 'id': 3}, + {'bucket_id': 100, 'name': 'Mike', 'id': 4}, + {'bucket_id': 200, 'name': 'Bill', 'id': 5}, + {'bucket_id': 300, 'name': 'Rob', 'id': 6}, + ], + }, + }, + 'error': { + 'input': { + 'args': [[],[]], + }, + 'output': { + 'str': [], + }, + }, + }, + 'crud_truncate': { + 'success': { + 'input': { + 'args': ['tester'], + }, + 'output': { + 'scalar': True, + }, + }, + 'error': { + 'input': { + 'args': ['no-such-space-name', {'timeout': 10}], + }, + 'output': { + 'str': [ + r'"no-such-space-name" doesn\'t exist', + ], + }, + }, + }, + 'crud_stats': { + 'success': { + 'input': { + 'args': ['tester'], + }, + 'output': { + 'operations': [ + 'insert', 'replace', + 'upsert', 'len', + 'delete', 'get', + 'select', 'borders', + 'update', 'count', + 'truncate', + ], + }, + }, + 'error': { + 'input': { + 'args': [], + }, + 'output': { + 'str': [], + }, + }, + }, + } + + def _correct_operation_with_crud(self, testing_function, case, mode=None): + if mode is not None: + resp = testing_function( + *case['success']['input']['args'], + mode=mode + ) + else: + resp = testing_function( + *case['success']['input']['args'], + ) + if 'rows' in case['success']['output']: + # Case for crud responce as tarantool.crud.CrudResult obj. + self.assertEqual(resp.rows, case['success']['output']['rows']) + if 'scalar' in case['success']['output']: + # Case for scalar value as crud responce, not tarantool.crud.CrudResult obj. + self.assertEqual(resp, case['success']['output']['scalar']) + if 'operations' in case['success']['output']: + # Case for statistics testing. + for operation in case['success']['output']['operations']: + self.assertEqual(operation in resp.__dict__, True, + 'Problem with finding a field with a statistic about operation ' + + operation) + + def _exception_operation_with_crud(self, testing_function, case, mode=None): + try: + if mode is not None: + _ = testing_function( + *case['error']['input']['args'], + mode=mode + ) + else: + _ = testing_function( + *case['error']['input']['args'], + ) + except DatabaseError as e: + for regexp_case in case['error']['output']['str']: + if hasattr(e, 'extra_info_error'): + # Case for non-batch operations. + self.assertNotEqual(re.search(regexp_case, e.extra_info_error.str), None) + if hasattr(e, 'errors_list'): + # Case for *_many() operations. + err_sum = str() + for err in e.errors_list: + err_sum = err_sum + err.str + self.assertNotEqual(re.search(regexp_case, err_sum), None) + if hasattr(e, 'success_list'): + # Case for *_many() operations. + if 'res_rows' in case['error']['output']: + self.assertEqual(e.success_list.rows, case['error']['output']['res_rows']) + + def test_crud_module_via_connection(self): + for case_name in self.crud_test_cases.keys(): + with self.subTest(name=case_name): + case = self.crud_test_cases[case_name] + testing_function = getattr(self.conn, case_name) + # Correct try testing. + self._correct_operation_with_crud(testing_function, case) + # Exception try testing. + self._exception_operation_with_crud(testing_function, case) + + def test_crud_module_via_mesh_connection(self): + for case_name in self.crud_test_cases.keys(): + with self.subTest(name=case_name): + case = self.crud_test_cases[case_name] + testing_function = getattr(self.conn_mesh, case_name) + # Correct try testing. + self._correct_operation_with_crud(testing_function, case) + # Exception try testing. + self._exception_operation_with_crud(testing_function, case) + + def test_crud_module_via_pool_connection(self): + for case_name in self.crud_test_cases.keys(): + with self.subTest(name=case_name): + case = self.crud_test_cases[case_name] + testing_function = getattr(self.conn_pool, case_name) + # Correct try testing. + self._correct_operation_with_crud(testing_function, case, mode=tarantool.Mode.RW) + # Exception try testing. + self._exception_operation_with_crud(testing_function, case, mode=tarantool.Mode.RW) + + def tearDown(self): + # Close connections to instance. + self.conn.close() + self.conn_mesh.close() + self.conn_pool.close() + + @classmethod + def tearDownClass(self): + # Stop instance. + self.srv.stop() + self.srv.clean()