This is an automated email from the ASF dual-hosted git repository.

isapego pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new f7153bb113 IGNITE-23126 DB API Driver 3: Implement execution with 
parameters (#4371)
f7153bb113 is described below

commit f7153bb113a055efa89c121ef8cc2a77d25f9ad3
Author: Igor Sapego <[email protected]>
AuthorDate: Wed Sep 11 11:00:30 2024 +0200

    IGNITE-23126 DB API Driver 3: Implement execution with parameters (#4371)
---
 .../cpp/ignite/odbc/app/parameter_set.cpp          |  50 ++--
 .../platforms/cpp/ignite/odbc/app/parameter_set.h  |  96 ++++++--
 .../platforms/cpp/ignite/odbc/query/data_query.cpp |   9 +-
 .../platforms/cpp/ignite/odbc/query/data_query.h   |   4 +-
 .../platforms/cpp/ignite/odbc/sql_statement.cpp    |  14 +-
 modules/platforms/cpp/ignite/odbc/sql_statement.h  |  19 +-
 modules/platforms/python/cpp_module/module.cpp     |  60 ++++-
 modules/platforms/python/cpp_module/module.h       |  26 ++
 .../platforms/python/cpp_module/py_connection.h    |   2 +
 modules/platforms/python/cpp_module/py_cursor.cpp  | 200 ++++++++-------
 modules/platforms/python/cpp_module/py_cursor.h    |   2 +
 .../platforms/python/cpp_module/type_conversion.h  | 200 +++++++++++++++
 modules/platforms/python/pyignite3/__init__.py     |  45 +++-
 modules/platforms/python/tests/conftest.py         |  39 +++
 modules/platforms/python/tests/test_connect.py     |  14 +-
 modules/platforms/python/tests/test_execute.py     | 104 +++-----
 .../platforms/python/tests/test_fetch_constants.py | 211 ++++------------
 .../python/tests/test_fetch_parameters.py          |  67 +++++
 modules/platforms/python/tests/test_fetch_table.py | 272 +++++++++------------
 modules/platforms/python/tests/util.py             |   2 +-
 20 files changed, 875 insertions(+), 561 deletions(-)

diff --git a/modules/platforms/cpp/ignite/odbc/app/parameter_set.cpp 
b/modules/platforms/cpp/ignite/odbc/app/parameter_set.cpp
index a000b9c8d4..e7654a49f4 100644
--- a/modules/platforms/cpp/ignite/odbc/app/parameter_set.cpp
+++ b/modules/platforms/cpp/ignite/odbc/app/parameter_set.cpp
@@ -22,79 +22,79 @@
 
 namespace ignite {
 
-void parameter_set::set_param_set_size(SQLULEN size) {
+void parameter_set_impl::set_param_set_size(SQLULEN size) {
     m_param_set_size = size;
 }
 
-void parameter_set::bind_parameter(std::uint16_t param_idx, const parameter 
&param) {
+void parameter_set_impl::bind_parameter(std::uint16_t param_idx, const 
parameter &param) {
     m_params[param_idx] = param;
 }
 
-void parameter_set::unbind_parameter(std::uint16_t param_idx) {
+void parameter_set_impl::unbind_parameter(std::uint16_t param_idx) {
     m_params.erase(param_idx);
 }
 
-void parameter_set::unbind_all() {
+void parameter_set_impl::unbind_all() {
     m_params.clear();
 }
 
-std::uint16_t parameter_set::get_parameters_number() const {
+std::uint16_t parameter_set_impl::get_parameters_number() const {
     return static_cast<std::uint16_t>(m_params.size());
 }
 
-void parameter_set::set_param_bind_offset_ptr(int *ptr) {
+void parameter_set_impl::set_param_bind_offset_ptr(int *ptr) {
     m_param_bind_offset = ptr;
 }
 
-int *parameter_set::get_param_bind_offset_ptr() {
+int *parameter_set_impl::get_param_bind_offset_ptr() {
     return m_param_bind_offset;
 }
 
-void parameter_set::prepare() {
+void parameter_set_impl::prepare() {
     m_param_set_pos = 0;
 
     for (auto &param : m_params)
         param.second.reset_stored_data();
 }
 
-bool parameter_set::is_data_at_exec_needed() const {
+bool parameter_set_impl::is_data_at_exec_needed() const {
     return std::any_of(
         m_params.begin(), m_params.end(), [](const auto &param) { return 
!param.second.is_data_ready(); });
 }
 
-void parameter_set::set_params_processed_ptr(SQLULEN *ptr) {
+void parameter_set_impl::set_params_processed_ptr(SQLULEN *ptr) {
     m_processed_param_rows = ptr;
 }
 
-SQLULEN *parameter_set::get_params_processed_ptr() const {
+SQLULEN *parameter_set_impl::get_params_processed_ptr() const {
     return m_processed_param_rows;
 }
 
-void parameter_set::set_params_status_ptr(SQLUSMALLINT *value) {
+void parameter_set_impl::set_params_status_ptr(SQLUSMALLINT *value) {
     m_params_status = value;
 }
 
-SQLUSMALLINT *parameter_set::get_params_status_ptr() const {
+SQLUSMALLINT *parameter_set_impl::get_params_status_ptr() const {
     return m_params_status;
 }
 
-void parameter_set::set_params_status(int64_t idx, SQLUSMALLINT status) const {
+void parameter_set_impl::set_params_status(int64_t idx, SQLUSMALLINT status) 
const {
     if (idx < 0 || !m_params_status || idx >= 
static_cast<int64_t>(m_param_set_size))
         return;
 
     m_params_status[idx] = status;
 }
 
-void parameter_set::set_params_processed(SQLULEN processed) const {
+void parameter_set_impl::set_params_processed(SQLULEN processed) {
     if (m_processed_param_rows)
         *m_processed_param_rows = processed;
 }
 
-bool parameter_set::is_parameter_selected() const {
+bool parameter_set_impl::is_parameter_selected() const {
     return m_current_param_idx != 0;
 }
 
-parameter *parameter_set::get_parameter(std::uint16_t idx) {
+parameter *parameter_set_impl::get_parameter(std::uint16_t idx) {
     auto it = m_params.find(idx);
     if (it != m_params.end())
         return &it->second;
@@ -102,7 +102,7 @@ parameter *parameter_set::get_parameter(std::uint16_t idx) {
     return nullptr;
 }
 
-const parameter *parameter_set::get_parameter(std::uint16_t idx) const {
+const parameter *parameter_set_impl::get_parameter(std::uint16_t idx) const {
     auto it = m_params.find(idx);
     if (it != m_params.end())
         return &it->second;
@@ -110,11 +110,11 @@ const parameter 
*parameter_set::get_parameter(std::uint16_t idx) const {
     return nullptr;
 }
 
-parameter *parameter_set::get_selected_parameter() {
+parameter *parameter_set_impl::get_selected_parameter() {
     return get_parameter(m_current_param_idx);
 }
 
-parameter *parameter_set::select_next_parameter() {
+parameter *parameter_set_impl::select_next_parameter() {
     for (auto it = m_params.begin(); it != m_params.end(); ++it) {
         std::uint16_t param_idx = it->first;
         parameter &param = it->second;
@@ -128,7 +128,7 @@ parameter *parameter_set::select_next_parameter() {
     return nullptr;
 }
 
-void parameter_set::write(protocol::writer &writer) const {
+void parameter_set_impl::write(protocol::writer &writer) const {
     auto args_num = calculate_row_len();
     if (args_num == 0) {
         writer.write_nil();
@@ -140,7 +140,7 @@ void parameter_set::write(protocol::writer &writer) const {
     write_row(writer, 0);
 }
 
-void parameter_set::write(protocol::writer &writer, SQLULEN begin, SQLULEN 
end, bool last) const {
+void parameter_set_impl::write(protocol::writer &writer, SQLULEN begin, 
SQLULEN end, bool last) const {
     std::int32_t row_len = calculate_row_len();
 
     writer.write(row_len);
@@ -160,7 +160,7 @@ void parameter_set::write(protocol::writer &writer, SQLULEN 
begin, SQLULEN end,
     }
 }
 
-void parameter_set::write_row(protocol::writer &writer, SQLULEN idx) const {
+void parameter_set_impl::write_row(protocol::writer &writer, SQLULEN idx) 
const {
     auto args_num = calculate_row_len();
     binary_tuple_builder row_builder{args_num * 3};
 
@@ -203,14 +203,14 @@ void parameter_set::write_row(protocol::writer &writer, 
SQLULEN idx) const {
     writer.write_binary(args_data);
 }
 
-std::int32_t parameter_set::calculate_row_len() const {
+std::int32_t parameter_set_impl::calculate_row_len() const {
     if (!m_params.empty())
         return static_cast<std::int32_t>(m_params.rbegin()->first);
 
     return 0;
 }
 
-std::int32_t parameter_set::get_param_set_size() const {
+std::int32_t parameter_set_impl::get_param_set_size() const {
     return static_cast<std::int32_t>(m_param_set_size);
 }
 
diff --git a/modules/platforms/cpp/ignite/odbc/app/parameter_set.h 
b/modules/platforms/cpp/ignite/odbc/app/parameter_set.h
index 1f2662e237..9f7c354388 100644
--- a/modules/platforms/cpp/ignite/odbc/app/parameter_set.h
+++ b/modules/platforms/cpp/ignite/odbc/app/parameter_set.h
@@ -30,6 +30,57 @@ namespace ignite {
  * Parameter set.
  */
 class parameter_set {
+public:
+    /**
+     * Default constructor.
+     */
+    parameter_set() = default;
+
+    /**
+     * Write only first row of the param set using provided writer.
+     *
+     * @param writer Writer.
+     */
+    virtual void write(protocol::writer &writer) const = 0;
+
+    /**
+     * Write rows of the param set in interval [begin, end) using provided 
writer.
+     *
+     * @param writer Writer.
+     * @param begin Beginning of the interval.
+     * @param end End of the interval.
+     * @param last Last page flag.
+     */
+    virtual void write(protocol::writer &writer, SQLULEN begin, SQLULEN end, 
bool last) const = 0;
+
+    /**
+     * Get parameter set size.
+     *
+     * @return Number of rows in set.
+     */
+    [[nodiscard]] virtual std::int32_t get_param_set_size() const = 0;
+
+    /**
+     * Set number of parameters processed in batch.
+     *
+     * @param processed Processed.
+     */
+    virtual void set_params_processed(SQLULEN processed) = 0;
+
+    /**
+     * Get pointer to array in which to return the status of each
+     * set of parameters.
+     *
+     * @return Value.
+     */
+    [[nodiscard]] virtual SQLUSMALLINT *get_params_status_ptr() const = 0;
+};
+
+
+/**
+ * Parameter set implementation.
+ */
+class parameter_set_impl : public parameter_set {
     /** Parameter binging map type alias. */
     typedef std::map<std::uint16_t, parameter> parameter_binding_map;
 
@@ -37,16 +88,16 @@ public:
     /**
      * Default constructor.
      */
-    parameter_set() = default;
+    parameter_set_impl() = default;
 
     // Deleted
-    parameter_set(parameter_set &&) = delete;
-    parameter_set(const parameter_set &) = delete;
-    parameter_set &operator=(parameter_set &&) = delete;
-    parameter_set &operator=(const parameter_set &) = delete;
+    parameter_set_impl(parameter_set_impl &&) = delete;
+    parameter_set_impl(const parameter_set_impl &) = delete;
+    parameter_set_impl &operator=(parameter_set_impl &&) = delete;
+    parameter_set_impl &operator=(const parameter_set_impl &) = delete;
 
     /**
-     * Set m_parameters set size.
+     * Set parameters set size.
      *
      * @param size Size of the parameter set.
      */
@@ -68,14 +119,14 @@ public:
     void unbind_parameter(std::uint16_t param_idx);
 
     /**
-     * Unbind all m_parameters.
+     * Unbind all parameters.
      */
     void unbind_all();
 
     /**
-     * Get number of bound m_parameters.
+     * Get number of bound parameters.
      *
-     * @return Number of bound m_parameters.
+     * @return Number of bound parameters.
      */
     [[nodiscard]] std::uint16_t get_parameters_number() const;
 
@@ -94,7 +145,7 @@ public:
     int *get_param_bind_offset_ptr();
 
     /**
-     * Prepare m_parameters set for statement execution.
+     * Prepare parameters set for statement execution.
      */
     void prepare();
 
@@ -146,16 +197,17 @@ public:
      * Write only first row of the param set using provided writer.
      * @param writer Writer.
      */
-    void write(protocol::writer &writer) const;
+    void write(protocol::writer &writer) const override;
 
     /**
      * Write rows of the param set in interval [begin, end) using provided 
writer.
+     *
      * @param writer Writer.
      * @param begin Beginning of the interval.
      * @param end End of the interval.
      * @param last Last page flag.
      */
-    void write(protocol::writer &writer, SQLULEN begin, SQLULEN end, bool 
last) const;
+    void write(protocol::writer &writer, SQLULEN begin, SQLULEN end, bool 
last) const override;
 
     /**
      * Calculate row length.
@@ -169,14 +221,14 @@ public:
      *
      * @return Number of rows in set.
      */
-    [[nodiscard]] std::int32_t get_param_set_size() const;
+    [[nodiscard]] std::int32_t get_param_set_size() const override;
 
     /**
-     * Set number of m_parameters processed in batch.
+     * Set number of parameters processed in batch.
      *
      * @param processed Processed.
      */
-    void set_params_processed(SQLULEN processed) const;
+    void set_params_processed(SQLULEN processed) override;
 
     /**
      * Number of processed params should be written using provided address.
@@ -186,25 +238,25 @@ public:
     void set_params_processed_ptr(SQLULEN *ptr);
 
     /**
-     * Get pointer to write number of m_parameters processed in batch.
+     * Get pointer to write number of parameters processed in batch.
      *
-     * @return Pointer to write number of m_parameters processed in batch.
+     * @return Pointer to write number of parameters processed in batch.
      */
     [[nodiscard]] SQLULEN *get_params_processed_ptr() const;
 
     /**
      * Set pointer to array in which to return the status of each
-     * set of m_parameters.
+     * set of parameters.
      * @param value Value.
      */
     void set_params_status_ptr(SQLUSMALLINT *value);
 
     /**
      * Get pointer to array in which to return the status of each
-     * set of m_parameters.
+     * set of parameters.
      * @return Value.
      */
-    [[nodiscard]] SQLUSMALLINT *get_params_status_ptr() const;
+    [[nodiscard]] SQLUSMALLINT *get_params_status_ptr() const override;
 
     /**
      * Set parameter status.
@@ -224,10 +276,10 @@ private:
     /** Parameters. */
     parameter_binding_map m_params{};
 
-    /** Offset added to pointers to change binding of m_parameters. */
+    /** Offset added to pointers to change binding of parameters. */
     int *m_param_bind_offset{nullptr};
 
-    /** Processed m_parameters. */
+    /** Processed parameters. */
     SQLULEN *m_processed_param_rows{nullptr};
 
     /** Parameters status. */
diff --git a/modules/platforms/cpp/ignite/odbc/query/data_query.cpp 
b/modules/platforms/cpp/ignite/odbc/query/data_query.cpp
index 64d3fda363..c189b1cf00 100644
--- a/modules/platforms/cpp/ignite/odbc/query/data_query.cpp
+++ b/modules/platforms/cpp/ignite/odbc/query/data_query.cpp
@@ -102,7 +102,7 @@ conversion_result 
put_primitive_to_buffer(application_data_buffer &buffer, const
 namespace ignite {
 
 data_query::data_query(diagnosable_adapter &m_diag, sql_connection 
&m_connection, std::string sql,
-    const parameter_set &params, std::int32_t &timeout)
+    parameter_set &params, std::int32_t &timeout)
     : query(m_diag, query_type::DATA)
     , m_connection(m_connection)
     , m_query(std::move(sql))
@@ -360,7 +360,7 @@ void data_query::process_affected_rows(const 
std::vector<std::int64_t> &affected
     for (auto &ar : affected_rows) {
         m_rows_affected += ar;
     }
-    m_params.set_params_processed(affected_rows.size());
+    m_params.set_params_processed(m_rows_affected);
 
     if (status_ptr) {
         for (auto i = 0; i < m_params.get_param_set_size(); i++) {
@@ -368,11 +368,6 @@ void data_query::process_affected_rows(const 
std::vector<std::int64_t> &affected
         }
     }
 
-    // Batch query, set attribute if it's set
-    if (auto affected = m_params.get_params_processed_ptr(); affected) {
-        *affected = m_rows_affected;
-    }
-
     m_executed = true;
 }
 
diff --git a/modules/platforms/cpp/ignite/odbc/query/data_query.h 
b/modules/platforms/cpp/ignite/odbc/query/data_query.h
index fb12b7b463..c1a0d1a4db 100644
--- a/modules/platforms/cpp/ignite/odbc/query/data_query.h
+++ b/modules/platforms/cpp/ignite/odbc/query/data_query.h
@@ -55,7 +55,7 @@ public:
      * @param params SQL params.
      * @param timeout Timeout.
      */
-    data_query(diagnosable_adapter &diag, sql_connection &connection, 
std::string sql, const parameter_set &params,
+    data_query(diagnosable_adapter &diag, sql_connection &connection, 
std::string sql, parameter_set &params,
         std::int32_t &timeout);
 
     /**
@@ -263,7 +263,7 @@ private:
     std::string m_query;
 
     /** Parameter bindings. */
-    const parameter_set &m_params;
+    parameter_set &m_params;
 
     /** Parameter types. */
     std::vector<sql_parameter> m_params_meta{};
diff --git a/modules/platforms/cpp/ignite/odbc/sql_statement.cpp 
b/modules/platforms/cpp/ignite/odbc/sql_statement.cpp
index bd24850034..bb3f2345e6 100644
--- a/modules/platforms/cpp/ignite/odbc/sql_statement.cpp
+++ b/modules/platforms/cpp/ignite/odbc/sql_statement.cpp
@@ -523,13 +523,25 @@ void sql_statement::execute_sql_query(const std::string 
&query) {
 
 sql_result sql_statement::internal_execute_sql_query(const std::string &query) 
{
     sql_result result = internal_prepare_sql_query(query);
-
     if (result != sql_result::AI_SUCCESS)
         return result;
 
     return internal_execute_sql_query();
 }
 
+void sql_statement::execute_sql_query(const std::string &query, parameter_set 
&params) {
+    IGNITE_ODBC_API_CALL(internal_execute_sql_query(query, params));
+}
+
+sql_result sql_statement::internal_execute_sql_query(const std::string &query, 
parameter_set &params) {
+    if (m_current_query)
+        m_current_query->close();
+
+    m_current_query = std::make_unique<data_query>(*this, m_connection, query, 
params, m_timeout);
+
+    return internal_execute_sql_query();
+}
+
 void sql_statement::execute_sql_query() {
     IGNITE_ODBC_API_CALL(internal_execute_sql_query());
 }
diff --git a/modules/platforms/cpp/ignite/odbc/sql_statement.h 
b/modules/platforms/cpp/ignite/odbc/sql_statement.h
index 3eeba5255f..6840759848 100644
--- a/modules/platforms/cpp/ignite/odbc/sql_statement.h
+++ b/modules/platforms/cpp/ignite/odbc/sql_statement.h
@@ -149,6 +149,14 @@ public:
      */
     void execute_sql_query(const std::string &query);
 
+    /**
+     * Execute SQL query with the custom parameter set.
+     *
+     * @param query SQL query.
+     * @param params Custom parameter set.
+     */
+    void execute_sql_query(const std::string &query, parameter_set &params);
+
     /**
      * Execute SQL query.
      */
@@ -467,6 +475,15 @@ private:
      */
     sql_result internal_execute_sql_query(const std::string &query);
 
+    /**
+     * Execute SQL query.
+     *
+     * @param query SQL query.
+     * @param params Custom parameter set.
+     * @return Operation result.
+     */
+    sql_result internal_execute_sql_query(const std::string &query, 
parameter_set &params);
+
     /**
      * Execute SQL query.
      *
@@ -667,7 +684,7 @@ private:
     SQLULEN m_row_array_size{1};
 
     /** Parameters. */
-    parameter_set m_parameters;
+    parameter_set_impl m_parameters;
 
     /** Query timeout in seconds. */
     std::int32_t m_timeout{0};
diff --git a/modules/platforms/python/cpp_module/module.cpp 
b/modules/platforms/python/cpp_module/module.cpp
index 9a7223e21b..c39b81196f 100644
--- a/modules/platforms/python/cpp_module/module.cpp
+++ b/modules/platforms/python/cpp_module/module.cpp
@@ -21,6 +21,7 @@
 
 #include <ignite/odbc/sql_environment.h>
 #include <ignite/odbc/sql_connection.h>
+#include <ignite/common/detail/defer.h>
 
 #include <memory>
 #include <cmath>
@@ -75,20 +76,49 @@ static PyObject* pyignite3_connect(PyObject* self, 
PyObject* args, PyObject* kwa
         nullptr
     };
 
-    const char* address = nullptr;
-    const char* identity = nullptr;
-    const char* secret = nullptr;
-    const char* schema = nullptr;
-    const char* timezone = nullptr;
-    double timeout = 0.0;
+    PyObject *address = nullptr;
+    const char *identity = nullptr;
+    const char *secret = nullptr;
+    const char *schema = nullptr;
+    const char *timezone = nullptr;
+    int timeout = 0;
     int page_size = 0;
 
     int parsed = PyArg_ParseTupleAndKeywords(
-        args, kwargs, "s|ssssdi", kwlist, &address, &identity, &secret, 
&schema, &timezone, &timeout, &page_size);
+        args, kwargs, "O|$ssssii", kwlist, &address, &identity, &secret, 
&schema, &timezone, &timeout, &page_size);
 
     if (!parsed)
         return nullptr;
 
+    std::stringstream address_builder;
+    if (PyList_Check(address)) {
+        auto size = PyList_Size(address);
+        for (Py_ssize_t idx = 0; idx < size; ++idx) {
+            auto item = PyList_GetItem(address, idx);
+            if (!PyUnicode_Check(item)) {
+                PyErr_SetString(PyExc_RuntimeError, "Only list of string 
values is allowed in 'address' parameter");
+                return nullptr;
+            }
+
+            auto str_array = PyUnicode_AsUTF8String(item);
+            if (!str_array) {
+                PyErr_SetString(PyExc_RuntimeError, "Can not convert address 
string to UTF-8");
+                return nullptr;
+            }
+            // To be called when the scope is left.
+            ignite::detail::defer([&] { Py_DECREF(str_array); });
+
+            auto *data = PyBytes_AsString(str_array);
+            auto len = PyBytes_Size(str_array);
+            std::string_view view(data, len);
+
+            address_builder << view;
+            if ((idx + 1) < size) {
+                address_builder << ',';
+            }
+        }
+    }
+
     using namespace ignite;
 
     auto sql_env = std::make_unique<sql_environment>();
@@ -98,7 +128,8 @@ static PyObject* pyignite3_connect(PyObject* self, PyObject* 
args, PyObject* kwa
         return nullptr;
 
     configuration cfg;
-    cfg.set_address(address);
+    auto addrs_str = address_builder.str();
+    cfg.set_address(addrs_str);
 
     if (schema)
         cfg.set_schema(schema);
@@ -112,10 +143,9 @@ static PyObject* pyignite3_connect(PyObject* self, 
PyObject* args, PyObject* kwa
     if (page_size)
         cfg.set_page_size(std::int32_t(page_size));
 
-    std::int32_t s_timeout = std::lround(timeout);
-    if (s_timeout)
+    if (timeout)
     {
-        void* ptr_timeout = (void*)(ptrdiff_t(s_timeout));
+        void* ptr_timeout = (void*)(ptrdiff_t(timeout));
         sql_conn->set_attribute(SQL_ATTR_CONNECTION_TIMEOUT, ptr_timeout, 0);
         if (!check_errors(*sql_conn))
             return nullptr;
@@ -192,6 +222,10 @@ bool check_errors(ignite::diagnosable& diag) {
     return false;
 }
 
+const char* py_object_get_typename(PyObject* obj) {
+    if (!obj || !obj->ob_type || !obj->ob_type->tp_name) {
+        return "Unknown";
+    }
 
-
-
+    return obj->ob_type->tp_name;
+}
diff --git a/modules/platforms/python/cpp_module/module.h 
b/modules/platforms/python/cpp_module/module.h
index 4832724e11..fdb910867f 100644
--- a/modules/platforms/python/cpp_module/module.h
+++ b/modules/platforms/python/cpp_module/module.h
@@ -15,10 +15,36 @@
  * limitations under the License.
  */
 
+#pragma once
+
+#include <Python.h>
+
 #define MODULE_NAME "_pyignite3_extension"
 
+#define PY_ASSERT(cond, err)                                \
+    do {                                                    \
+        if (!(cond)) {                                      \
+            PyErr_SetString(PyExc_AssertionError, (err));   \
+            return nullptr;                                 \
+        }                                                   \
+    } while (false)
+
+
 namespace ignite {
 class diagnosable;
 }
 
+/**
+ * Check odbc object for errors, and set a proper Python exception, if there 
are.
+ * @param diag Diagnosable object instance.
+ * @return @c true if there is no error, and @c false, if there is an error.
+ */
 bool check_errors(ignite::diagnosable& diag);
+
+/**
+ * Get a typename of the PyObject instance safely, if possible.
+ *
+ * @param obj Object.
+ * @return Typename if available, and "Unknown" otherwise.
+ */
+const char* py_object_get_typename(PyObject* obj);
diff --git a/modules/platforms/python/cpp_module/py_connection.h 
b/modules/platforms/python/cpp_module/py_connection.h
index 246ac3b0a6..b6da49061b 100644
--- a/modules/platforms/python/cpp_module/py_connection.h
+++ b/modules/platforms/python/cpp_module/py_connection.h
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+#pragma once
+
 #include <memory>
 
 #include <Python.h>
diff --git a/modules/platforms/python/cpp_module/py_cursor.cpp 
b/modules/platforms/python/cpp_module/py_cursor.cpp
index ca9fbca610..4506249f0a 100644
--- a/modules/platforms/python/cpp_module/py_cursor.cpp
+++ b/modules/platforms/python/cpp_module/py_cursor.cpp
@@ -18,13 +18,106 @@
 #include <ignite/odbc/sql_statement.h>
 #include <ignite/odbc/query/data_query.h>
 
-#include <ignite/common/detail/config.h>
-
 #include "module.h"
 #include "py_cursor.h"
+#include "type_conversion.h"
 
 #include <Python.h>
 
+/**
+ * Python parameter set.
+ */
+class py_parameter_set : public ignite::parameter_set {
+public:
+    /**
+     * Constructor.
+     *
+     * @param params Python parameters sequence.
+     */
+    py_parameter_set(Py_ssize_t size, PyObject *params) : m_size(size), 
m_params(params) {}
+
+    /**
+     * Write only first row of the param set using provided writer.
+     *
+     * @param writer Writer.
+     */
+    virtual void write(ignite::protocol::writer &writer) const override {
+        auto row_size = std::int32_t(m_size);
+        if (!row_size) {
+            writer.write_nil();
+            return;
+        }
+
+        writer.write(row_size);
+        ignite::binary_tuple_builder row_builder{row_size * 3};
+        row_builder.start();
+
+        for (std::int32_t idx = 0; idx < row_size; ++idx) {
+            submit_pyobject(row_builder, PySequence_GetItem(m_params, idx), 
true);
+        }
+
+        row_builder.layout();
+
+        for (std::int32_t idx = 0; idx < row_size; ++idx) {
+            submit_pyobject(row_builder, PySequence_GetItem(m_params, idx), 
false);
+        }
+
+        auto row_data = row_builder.build();
+        writer.write_binary(row_data);
+    }
+
+    /**
+     * Write rows of the param set in interval [begin, end) using provided 
writer.
+     *
+     * @param writer Writer.
+     * @param begin Beginning of the interval.
+     * @param end End of the interval.
+     * @param last Last page flag.
+     */
+    virtual void write(ignite::protocol::writer &writer, SQLULEN begin, 
SQLULEN end, bool last) const override {
+        // TODO: IGNITE-22742 Implement execution with a batch of parameters
+        throw ignite::ignite_error("Execution with the batch of parameters is 
not implemented");
+    }
+
+    /**
+     * Get parameter set size.
+     *
+     * @return Number of rows in set.
+     */
+    [[nodiscard]] virtual std::int32_t get_param_set_size() const override {
+        // TODO: IGNITE-22742 Implement execution with a batch of parameters
+        return 1;
+    }
+
+    /**
+     * Set number of parameters processed in batch.
+     *
+     * @param processed Processed.
+     */
+    virtual void set_params_processed(SQLULEN processed) override { 
m_processed = processed; }
+
+    /**
+     * Get pointer to array in which to return the status of each set of 
parameters.
+     *
+     * @return Value.
+     */
+    [[nodiscard]] virtual SQLUSMALLINT *get_params_status_ptr() const override 
{
+        // TODO: IGNITE-22742 Implement execution with a batch of parameters
+        return nullptr;
+    }
+
+private:
+    /** Size. */
+    Py_ssize_t m_size{0};
+
+    /** Python sequence of parameters. */
+    PyObject *m_params{nullptr};
+
+    /** Processed params. */
+    SQLULEN m_processed{0};
+};
+
+
 int py_cursor_init(py_cursor *self, PyObject *args, PyObject *kwds)
 {
     UNUSED_VALUE args;
@@ -71,15 +164,31 @@ static PyObject* py_cursor_execute(py_cursor* self, 
PyObject* args, PyObject* kw
     };
 
     const char* query = nullptr;
-    // TODO IGNITE-23126 Support parameters
     PyObject *params = nullptr;
 
     int parsed = PyArg_ParseTupleAndKeywords(args, kwargs, "s|O", kwlist, 
&query, &params);
-
     if (!parsed)
         return nullptr;
 
-    self->m_statement->execute_sql_query(query);
+    Py_ssize_t size{0};
+    if (params && params != Py_None) {
+        if (PySequence_Check(params)) {
+            size = PySequence_Size(params);
+            if (size < 0) {
+                PyErr_SetString(PyExc_RuntimeError, "Internal error while 
getting size of the parameters sequence");
+                return nullptr;
+            }
+        } else {
+            auto msg_str = std::string("The object does not provide the 
sequence protocol: ")
+                + py_object_get_typename(params);
+
+            PyErr_SetString(PyExc_RuntimeError, msg_str.c_str());
+            return nullptr;
+        }
+    }
+
+    py_parameter_set py_params(size, params);
+    self->m_statement->execute_sql_query(query, py_params);
     if (!check_errors(*self->m_statement))
         return nullptr;
 
@@ -102,83 +211,6 @@ static PyObject* py_cursor_rowcount(py_cursor* self, 
PyObject*)
     return PyLong_FromLong(long(query->affected_rows()));
 }
 
-static PyObject* primitive_to_pyobject(ignite::primitive value) {
-    using ignite::ignite_type;
-
-    if (value.is_null()) {
-        Py_INCREF(Py_None);
-        return Py_None;
-    }
-
-    switch (value.get_type()) {
-        case ignite_type::STRING: {
-            auto &str_val = value.get<std::string>();
-            return PyUnicode_FromStringAndSize(str_val.c_str(), 
str_val.size());
-        }
-
-        case ignite_type::INT8: {
-            auto &i8_val =  value.get<std::int8_t>();
-            return PyLong_FromLong(long(i8_val));
-        }
-
-        case ignite_type::INT16: {
-            auto &i16_val =  value.get<std::int16_t>();
-            return PyLong_FromLong(long(i16_val));
-        }
-
-        case ignite_type::INT32: {
-            auto &i32_val =  value.get<std::int32_t>();
-            return PyLong_FromLong(long(i32_val));
-        }
-
-        case ignite_type::INT64: {
-            auto &i64_val =  value.get<std::int64_t>();
-            return PyLong_FromLongLong(i64_val);
-        }
-
-        case ignite_type::FLOAT: {
-            auto &float_val =  value.get<float>();
-            return PyFloat_FromDouble(float_val);
-        }
-
-        case ignite_type::DOUBLE: {
-            auto &double_val =  value.get<double>();
-            return PyFloat_FromDouble(double_val);
-        }
-
-        case ignite_type::BOOLEAN: {
-            auto &bool_val =  value.get<bool>();
-            if (bool_val) {
-                Py_RETURN_TRUE;
-            } else {
-                Py_RETURN_FALSE;
-            }
-        }
-
-        case ignite_type::BYTE_ARRAY: {
-            auto &blob_val =  value.get<std::vector<std::byte>>();
-            return PyBytes_FromStringAndSize((const char*)blob_val.data(), 
blob_val.size());
-        }
-
-        case ignite_type::UUID:
-        case ignite_type::DATE:
-        case ignite_type::TIMESTAMP:
-        case ignite_type::TIME:
-        case ignite_type::DATETIME:
-        case ignite_type::BITMASK:
-        case ignite_type::DECIMAL:
-        case ignite_type::PERIOD:
-        case ignite_type::DURATION:
-        case ignite_type::NUMBER:
-        default: {
-            // TODO: IGNITE-22745 Provide wider data types support
-            auto err_msg = "The type is not supported yet: " + 
std::to_string(int(value.get_type()));
-            PyErr_SetString(PyExc_RuntimeError, err_msg.c_str());
-            return nullptr;
-        }
-    }
-}
-
 static PyObject* py_cursor_fetchone(py_cursor* self, PyObject*)
 {
     if (!self->m_statement) {
@@ -314,7 +346,7 @@ static PyObject* py_cursor_column_type_code(py_cursor* 
self, PyObject* args)
     return PyLong_FromLong(long(column->get_data_type()));
 }
 
-static PyObject* py_cursor_column_display_size(py_cursor* self, PyObject* args)
+static PyObject* py_cursor_column_display_size(py_cursor* self, PyObject*)
 {
     if (!self->m_statement) {
         PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state 
(Already closed?)");
@@ -325,7 +357,7 @@ static PyObject* py_cursor_column_display_size(py_cursor* 
self, PyObject* args)
     return Py_None;
 }
 
-static PyObject* py_cursor_column_internal_size(py_cursor* self, PyObject* 
args)
+static PyObject* py_cursor_column_internal_size(py_cursor* self, PyObject*)
 {
     if (!self->m_statement) {
         PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state 
(Already closed?)");
diff --git a/modules/platforms/python/cpp_module/py_cursor.h 
b/modules/platforms/python/cpp_module/py_cursor.h
index 035b21f1ba..e68f9e4bab 100644
--- a/modules/platforms/python/cpp_module/py_cursor.h
+++ b/modules/platforms/python/cpp_module/py_cursor.h
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+#pragma once
+
 #include <memory>
 
 #include <Python.h>
diff --git a/modules/platforms/python/cpp_module/type_conversion.h 
b/modules/platforms/python/cpp_module/type_conversion.h
new file mode 100644
index 0000000000..af0b0f13be
--- /dev/null
+++ b/modules/platforms/python/cpp_module/type_conversion.h
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "module.h"
+
+#include <ignite/protocol/writer.h>
+#include <ignite/common/ignite_type.h>
+#include <ignite/common/primitive.h>
+#include <ignite/common/detail/defer.h>
+#include <ignite/tuple/binary_tuple_builder.h>
+#include <ignite/protocol/utils.h>
+
+#include <optional>
+
+#include <Python.h>
+
+
+static PyObject* primitive_to_pyobject(ignite::primitive value) {
+    using ignite::ignite_type;
+
+    if (value.is_null()) {
+        Py_INCREF(Py_None);
+        return Py_None;
+    }
+
+    switch (value.get_type()) {
+        case ignite_type::STRING: {
+            auto &str_val = value.get<std::string>();
+            return PyUnicode_FromStringAndSize(str_val.c_str(), 
str_val.size());
+        }
+
+        case ignite_type::INT8: {
+            auto &i8_val =  value.get<std::int8_t>();
+            return PyLong_FromLong(long(i8_val));
+        }
+
+        case ignite_type::INT16: {
+            auto &i16_val =  value.get<std::int16_t>();
+            return PyLong_FromLong(long(i16_val));
+        }
+
+        case ignite_type::INT32: {
+            auto &i32_val =  value.get<std::int32_t>();
+            return PyLong_FromLong(long(i32_val));
+        }
+
+        case ignite_type::INT64: {
+            auto &i64_val =  value.get<std::int64_t>();
+            return PyLong_FromLongLong(i64_val);
+        }
+
+        case ignite_type::FLOAT: {
+            auto &float_val =  value.get<float>();
+            return PyFloat_FromDouble(float_val);
+        }
+
+        case ignite_type::DOUBLE: {
+            auto &double_val =  value.get<double>();
+            return PyFloat_FromDouble(double_val);
+        }
+
+        case ignite_type::BOOLEAN: {
+            auto &bool_val =  value.get<bool>();
+            if (bool_val) {
+                Py_RETURN_TRUE;
+            } else {
+                Py_RETURN_FALSE;
+            }
+        }
+
+        case ignite_type::BYTE_ARRAY: {
+            auto &blob_val =  value.get<std::vector<std::byte>>();
+            return PyBytes_FromStringAndSize((const char*)blob_val.data(), 
blob_val.size());
+        }
+
+        case ignite_type::UUID:
+        case ignite_type::DATE:
+        case ignite_type::TIMESTAMP:
+        case ignite_type::TIME:
+        case ignite_type::DATETIME:
+        case ignite_type::BITMASK:
+        case ignite_type::DECIMAL:
+        case ignite_type::PERIOD:
+        case ignite_type::DURATION:
+        case ignite_type::NUMBER:
+        default: {
+            // TODO: IGNITE-22745 Provide wider data types support
+            auto err_msg = "The type is not supported yet: " + 
std::to_string(int(value.get_type()));
+            PyErr_SetString(PyExc_RuntimeError, err_msg.c_str());
+            return nullptr;
+        }
+    }
+}
+
+static void submit_pyobject(ignite::binary_tuple_builder &builder, PyObject 
*obj, bool claim) {
+    if (obj == Py_None) {
+        if (claim) {
+            builder.claim_null();
+            builder.claim_null();
+            builder.claim_null();
+        } else {
+            builder.append_null();
+            builder.append_null();
+            builder.append_null();
+        }
+        return;
+    }
+
+    if (PyBool_Check(obj)) {
+        bool val = (obj == Py_True);
+        if (claim) {
+            ignite::protocol::claim_type_and_scale(builder, 
ignite::ignite_type::BOOLEAN);
+            builder.claim_bool(val);
+        } else {
+            ignite::protocol::append_type_and_scale(builder, 
ignite::ignite_type::BOOLEAN);
+            builder.append_bool(val);
+        }
+        return;
+    }
+
+    if (PyBytes_Check(obj)) {
+        auto *data = reinterpret_cast<std::byte*>(PyBytes_AsString(obj));
+        auto len = PyBytes_Size(obj);
+        ignite::bytes_view view(data, len);
+
+        if (claim) {
+            ignite::protocol::claim_type_and_scale(builder, 
ignite::ignite_type::BYTE_ARRAY);
+            builder.claim_varlen(view);
+        } else {
+            ignite::protocol::append_type_and_scale(builder, 
ignite::ignite_type::BYTE_ARRAY);
+            builder.append_varlen(view);
+        }
+        return;
+    }
+
+    if (PyUnicode_Check(obj)) {
+        auto str_array = PyUnicode_AsUTF8String(obj);
+        if (!str_array) {
+            throw ignite::ignite_error("Can not convert string to UTF-8");
+        }
+        // To be called when the scope is left.
+        ignite::detail::defer([&] { Py_DECREF(str_array); });
+
+        auto *data = PyBytes_AsString(str_array);
+        auto len = PyBytes_Size(str_array);
+        std::string_view view(data, len);
+
+        if (claim) {
+            ignite::protocol::claim_type_and_scale(builder, 
ignite::ignite_type::STRING);
+            builder.claim_varlen(view);
+        } else {
+            ignite::protocol::append_type_and_scale(builder, 
ignite::ignite_type::STRING);
+            builder.append_varlen(view);
+        }
+        return;
+    }
+
+    if (PyFloat_Check(obj)) {
+        double val = PyFloat_AsDouble(obj);
+        if (claim) {
+            ignite::protocol::claim_type_and_scale(builder, 
ignite::ignite_type::DOUBLE);
+            builder.claim_double(val);
+        } else {
+            ignite::protocol::append_type_and_scale(builder, 
ignite::ignite_type::DOUBLE);
+            builder.append_double(val);
+        }
+        return;
+    }
+
+    if (PyLong_Check(obj)) {
+        auto val = PyLong_AsLongLong(obj);
+        if (claim) {
+            ignite::protocol::claim_type_and_scale(builder, 
ignite::ignite_type::INT64);
+            builder.claim_int64(val);
+        } else {
+            ignite::protocol::append_type_and_scale(builder, 
ignite::ignite_type::INT64);
+            builder.append_int64(val);
+        }
+        return;
+    }
+
+    // TODO: IGNITE-22745 Provide wider data types support
+    throw ignite::ignite_error("Type is not supported " + 
std::string(py_object_get_typename(obj)));
+}
diff --git a/modules/platforms/python/pyignite3/__init__.py 
b/modules/platforms/python/pyignite3/__init__.py
index baf9cf29fb..1c9730e97d 100644
--- a/modules/platforms/python/pyignite3/__init__.py
+++ b/modules/platforms/python/pyignite3/__init__.py
@@ -15,7 +15,7 @@
 import datetime
 import decimal
 import uuid
-from typing import Optional, List, Any, Sequence
+from typing import Optional, List, Any, Sequence, Tuple, Union
 
 from pyignite3 import _pyignite3_extension
 from pyignite3 import native_type_code
@@ -44,7 +44,7 @@ DATETIME = datetime.datetime
 UUID = uuid.UUID
 
 
-def type_code_from_int(native: int):
+def _type_code_from_int(native: int):
     if native == native_type_code.NIL:
         return NIL
     elif native == native_type_code.BOOLEAN:
@@ -79,7 +79,7 @@ class ColumnDescription:
     def __init__(self, name: str, type_code: int, display_size: Optional[int], 
internal_size: Optional[int],
                  precision: Optional[int], scale: Optional[int], null_ok: 
bool):
         self.name = name
-        self.type_code = type_code_from_int(type_code)
+        self.type_code = _type_code_from_int(type_code)
         self.display_size = display_size
         self.internal_size = internal_size
         self.precision = precision
@@ -90,12 +90,17 @@ class ColumnDescription:
 class Cursor:
     """
     Cursor class. Represents a single statement and holds the result of its 
execution.
+
+    Attributes
+    ----------
+    arraysize: int
+        a read/write attribute, that specifies the number of rows to fetch at 
a time with .fetchmany().
+        It defaults to 1 meaning to fetch a single row at a time.
     """
+    arraysize: int = 1
 
     def __init__(self, py_cursor):
         self._py_cursor = py_cursor
-
-        self.arraysize = 1
         self._description = None
 
     def __enter__(self):
@@ -152,7 +157,7 @@ class Cursor:
             self._py_cursor.close()
             self._py_cursor = None
 
-    def execute(self, *args):
+    def execute(self, query: str, params: Optional[Union[List[Any], 
Tuple[Any]]] = None):
         """
         Execute a database operation (query or command).
 
@@ -165,7 +170,7 @@ class Cursor:
         if self._py_cursor is None:
             raise InterfaceError('Connection is already closed')
 
-        self._py_cursor.execute(*args)
+        self._py_cursor.execute(query, params)
         self._update_description()
 
     def _update_description(self):
@@ -315,17 +320,39 @@ class Connection:
         return Cursor(self._py_connection.cursor())
 
 
-def connect(**kwargs) -> Connection:
+def connect(address: [str], **kwargs) -> Connection:
     """
     Establish connection with the Ignite cluster.
+
+    Parameters
+    ----------
+    address: [str]
+        A list of addresses of cluster nodes for client to choose from. Used 
for initial connection and fail-over.
+
+    Keyword Arguments
+    ----------
+    identity: str, optional
+        An identifier to use for authentication. E.g. username.
+    secret: str, optional
+        A secret to use for authentication. E.g. password.
+    schema: str, optional
+        A schema name to be used by default. Default value: 'PUBLIC'.
+    timezone: str, optional
+        A timezone to use as a client's timezone. Used to correctly work with 
date/time values, received from client.
+        By default, a server's timezone is used.
+    page_size: int, optional
+        A maximum number of rows, which are received or sent in a single 
request. Default value: 1024.
+    timeout: int, optional
+        A timeout in seconds to use for any network operation. Default value: 
30.
     """
-    return _pyignite3_extension.connect(**kwargs)
+    return _pyignite3_extension.connect(address=address, **kwargs)
 
 
 class Error(Exception):
     pass
 
 
+# noinspection PyShadowingBuiltins
 class Warning(Exception):
     pass
 
diff --git a/modules/platforms/python/tests/conftest.py 
b/modules/platforms/python/tests/conftest.py
index 324c7d3884..bbacc5b7e0 100644
--- a/modules/platforms/python/tests/conftest.py
+++ b/modules/platforms/python/tests/conftest.py
@@ -14,5 +14,44 @@
 # limitations under the License.
 import logging
 
+import pyignite3
+import pytest
+
+from tests.util import check_cluster_started, start_cluster_gen, 
server_addresses_basic
+
 logger = logging.getLogger('pyignite3')
 logger.setLevel(logging.DEBUG)
+
+
[email protected]()
+def table_name(request):
+    return request.node.originalname
+
+
[email protected]()
+def connection():
+    conn = pyignite3.connect(address=server_addresses_basic)
+    yield conn
+    conn.close()
+
+
[email protected]()
+def cursor(connection):
+    cursor = connection.cursor()
+    yield cursor
+    cursor.close()
+
+
[email protected]()
+def drop_table_cleanup(cursor, table_name):
+    yield None
+    cursor.execute(f'drop table if exists {table_name}')
+
+
[email protected](autouse=True, scope="session")
+def cluster():
+    if not check_cluster_started():
+        yield from start_cluster_gen()
+    else:
+        yield None
+
diff --git a/modules/platforms/python/tests/test_connect.py 
b/modules/platforms/python/tests/test_connect.py
index 44e6fec6ea..81cd223ac0 100644
--- a/modules/platforms/python/tests/test_connect.py
+++ b/modules/platforms/python/tests/test_connect.py
@@ -15,24 +15,16 @@
 import pytest
 
 import pyignite3
-from tests.util import start_cluster_gen, check_cluster_started, 
server_addresses_invalid, server_addresses_basic
-
-
[email protected](autouse=True)
-def cluster():
-    if not check_cluster_started():
-        yield from start_cluster_gen()
-    else:
-        yield None
+from tests.util import server_addresses_invalid, server_addresses_basic
 
 
 def test_connection_success():
-    conn = pyignite3.connect(address=server_addresses_basic[0])
+    conn = pyignite3.connect(address=server_addresses_basic, timeout=1)
     assert conn is not None
     conn.close()
 
 
 def test_connection_fail():
     with pytest.raises(RuntimeError) as err:
-        pyignite3.connect(address=server_addresses_invalid[0])
+        pyignite3.connect(address=server_addresses_invalid)
     assert err.match("Failed to establish connection with the host.")
diff --git a/modules/platforms/python/tests/test_execute.py 
b/modules/platforms/python/tests/test_execute.py
index 510f7505d5..62f0e8ef97 100644
--- a/modules/platforms/python/tests/test_execute.py
+++ b/modules/platforms/python/tests/test_execute.py
@@ -12,88 +12,52 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-import pytest
-
 import pyignite3
-from tests.util import start_cluster_gen, check_cluster_started, 
server_addresses_basic
-
-
[email protected](autouse=True)
-def cluster():
-    if not check_cluster_started():
-        yield from start_cluster_gen()
-    else:
-        yield None
-
-
-def test_execute_const_sql_success():
-    conn = pyignite3.connect(address=server_addresses_basic[0])
-    assert conn is not None
-    try:
-        cursor = conn.cursor()
-        assert cursor is not None
-
-        try:
-            cursor.execute("select 1, 'Lorem Ipsum'")
-            assert cursor.rowcount == -1
 
-            assert cursor.description is not None
-            assert len(cursor.description) == 2
 
-            assert cursor.description[0].name == '1'
-            assert cursor.description[0].type_code == pyignite3.INT
-            assert cursor.description[0].null_ok is False
+def test_execute_const_sql_success(cursor):
+    cursor.execute("select 1, 'Lorem Ipsum'")
+    assert cursor.rowcount == -1
 
-            assert cursor.description[1].name == "'Lorem Ipsum'"
-            assert cursor.description[1].type_code == pyignite3.STRING
-            assert cursor.description[1].null_ok is False
-        finally:
-            cursor.close()
-    finally:
-        conn.close()
+    assert cursor.description is not None
+    assert len(cursor.description) == 2
 
+    assert cursor.description[0].name == '1'
+    assert cursor.description[0].type_code == pyignite3.INT
+    assert cursor.description[0].null_ok is False
 
-def test_execute_sql_table_success():
-    table_name = test_execute_update_rowcount.__name__
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            try:
-                cursor.execute(f'create table {table_name}(id int primary key, 
data varchar, dec decimal(3,5))')
-                cursor.execute(f"select id, data, dec from {table_name}")
+    assert cursor.description[1].name == "'Lorem Ipsum'"
+    assert cursor.description[1].type_code == pyignite3.STRING
+    assert cursor.description[1].null_ok is False
 
-                assert cursor.description is not None
-                assert len(cursor.description) == 3
 
-                assert cursor.description[0].name == 'ID'
-                assert cursor.description[0].type_code == pyignite3.INT
-                assert cursor.description[0].null_ok is False
+def test_execute_sql_table_success(table_name, cursor, drop_table_cleanup):
+    cursor.execute(f'create table {table_name}(id int primary key, data 
varchar, dec decimal(3,5))')
+    cursor.execute(f"select id, data, dec from {table_name}")
 
-                assert cursor.description[1].name == 'DATA'
-                assert cursor.description[1].type_code == pyignite3.STRING
-                assert cursor.description[1].null_ok is True
+    assert cursor.description is not None
+    assert len(cursor.description) == 3
 
-                assert cursor.description[2].name == 'DEC'
-                assert cursor.description[2].type_code == pyignite3.NUMBER
-                assert cursor.description[2].null_ok is True
-                assert cursor.description[2].scale == 5
-                assert cursor.description[2].precision == 3
+    assert cursor.description[0].name == 'ID'
+    assert cursor.description[0].type_code == pyignite3.INT
+    assert cursor.description[0].null_ok is False
 
-            finally:
-                cursor.execute(f'drop table if exists {table_name}')
+    assert cursor.description[1].name == 'DATA'
+    assert cursor.description[1].type_code == pyignite3.STRING
+    assert cursor.description[1].null_ok is True
 
+    assert cursor.description[2].name == 'DEC'
+    assert cursor.description[2].type_code == pyignite3.NUMBER
+    assert cursor.description[2].null_ok is True
+    assert cursor.description[2].scale == 5
+    assert cursor.description[2].precision == 3
 
-def test_execute_update_rowcount():
-    table_name = test_execute_update_rowcount.__name__
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            try:
-                cursor.execute(f'create table {table_name}(id int primary key, 
data varchar)')
-                for key in range(10):
-                    cursor.execute(f"insert into {table_name} values({key}, 
'data-{key*2}')")
-                    assert cursor.rowcount == 1
 
-                cursor.execute(f"update {table_name} set data='Lorem ipsum' 
where id > 3")
-                assert cursor.rowcount == 6
+def test_execute_update_rowcount(table_name, cursor, drop_table_cleanup):
+    cursor.execute(f'create table {table_name}(id int primary key, data 
varchar)')
+    for key in range(10):
+        cursor.execute(f"insert into {table_name} values({key}, 
'data-{key*2}')")
+        assert cursor.rowcount == 1
 
-            finally:
-                cursor.execute(f'drop table if exists {table_name}')
+    cursor.execute(f"update {table_name} set data='Lorem ipsum' where id > 3")
+    assert cursor.rowcount == 6
diff --git a/modules/platforms/python/tests/test_fetch_constants.py 
b/modules/platforms/python/tests/test_fetch_constants.py
index 3fd7929e13..cbd94e7736 100644
--- a/modules/platforms/python/tests/test_fetch_constants.py
+++ b/modules/platforms/python/tests/test_fetch_constants.py
@@ -17,172 +17,61 @@ import math
 import pytest
 
 import pyignite3
-from tests.util import start_cluster_gen, check_cluster_started, 
server_addresses_basic
-
-
[email protected](autouse=True)
-def cluster():
-    if not check_cluster_started():
-        yield from start_cluster_gen()
+from tests.util import server_addresses_basic
+
+
+test_data = [
+    ("select 'Lorem ipsum'", 'Lorem ipsum'),
+    ("select ''", ''),
+    ("select CAST(42 AS TINYINT)", 42),
+    ("select CAST(-18 AS TINYINT)", -18),
+    ("select CAST(4242 AS SMALLINT)", 4242),
+    ("select 987654321", 987654321),
+    ("select CAST(1234567890987654321 AS BIGINT)", 1234567890987654321),
+    ("select CAST(123.456 AS REAL)", 123.456),
+    ("select CAST(-123456789.987654321 AS DOUBLE)", -123456789.987654321),
+    ("select TRUE", True),
+    ("select FALSE", False),
+    ("select x'45F0AB'", b'\x45\xf0\xab'),
+    ("select x''", b''),
+    ("select NULL", None),
+]
+
+
[email protected]("query,value", test_data)
+def test_fetch_constant(query, value, cursor):
+    cursor.execute(query)
+    data = cursor.fetchone()
+    assert len(data) == 1
+    if isinstance(value, float):
+        assert data[0] == pytest.approx(value)
     else:
-        yield None
-
-
-def test_fetch_constant_string():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select 'Lorem ipsum'")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] == 'Lorem ipsum'
-
-
-def test_fetch_constant_string_empty():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select ''")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] == ''
-
-
-def test_fetch_constant_tinyint():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select CAST(42 AS TINYINT)")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] == 42
-
-
-def test_fetch_constant_tinyint_negative():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select CAST(-18 AS TINYINT)")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] == -18
-
-
-def test_fetch_constant_smallint():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select CAST(4242 AS SMALLINT)")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] == 4242
-
-
-def test_fetch_constant_int():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select 987654321")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] == 987654321
-
-
-def test_fetch_constant_bigint():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select CAST(1234567890987654321 AS BIGINT)")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] == 1234567890987654321
-
-
-def test_fetch_constant_real():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select CAST(123.456 AS REAL)")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] == pytest.approx(123.456)
-
-
-def test_fetch_constant_double():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select CAST(-123456789.987654321 AS DOUBLE)")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] == pytest.approx(-123456789.987654321)
-
-
-def test_fetch_constant_double_nan():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select CAST('NaN' AS DOUBLE)")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert math.isnan(data[0])
-
-
-def test_fetch_constant_bool_true():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select TRUE")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] is True
-
-
-def test_fetch_constant_bool_false():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select FALSE")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] is False
-
-
-def test_fetch_constant_binary():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select x'45F0AB'")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] == b'\x45\xf0\xab'
-
-
-def test_fetch_constant_binary_empty():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select x''")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] == b''
+        assert data[0] == value
 
 
-def test_fetch_constant_null():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select NULL")
-            data = cursor.fetchone()
-            assert len(data) == 1
-            assert data[0] is None
+def test_fetch_constant_double_nan(cursor):
+    cursor.execute("select CAST('NaN' AS DOUBLE)")
+    data = cursor.fetchone()
+    assert len(data) == 1
+    assert math.isnan(data[0])
 
 
-def test_fetch_constant_several_ints():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select 1,2,3")
-            data = cursor.fetchone()
-            assert len(data) == 3
-            assert data[0] == 1
-            assert data[1] == 2
-            assert data[2] == 3
+def test_fetch_constant_several_ints(cursor):
+    cursor.execute("select 1,2,3")
+    data = cursor.fetchone()
+    assert len(data) == 3
+    assert data[0] == 1
+    assert data[1] == 2
+    assert data[2] == 3
 
 
-def test_fetch_constant_int_bool_string():
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            cursor.execute("select 42, TRUE, 'Test string'")
-            data = cursor.fetchone()
-            assert len(data) == 3
-            assert data[0] == 42
-            assert data[1] is True
-            assert data[2] == 'Test string'
+def test_fetch_constant_int_bool_string(cursor):
+    cursor.execute("select 42, TRUE, 'Test string'")
+    data = cursor.fetchone()
+    assert len(data) == 3
+    assert data[0] == 42
+    assert data[1] is True
+    assert data[2] == 'Test string'
 
-            nothing = cursor.fetchone()
-            assert nothing is None
+    nothing = cursor.fetchone()
+    assert nothing is None
diff --git a/modules/platforms/python/tests/test_fetch_parameters.py 
b/modules/platforms/python/tests/test_fetch_parameters.py
new file mode 100644
index 0000000000..cf809908ec
--- /dev/null
+++ b/modules/platforms/python/tests/test_fetch_parameters.py
@@ -0,0 +1,67 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import pytest
+
+import pyignite3
+from tests.util import server_addresses_basic
+
+test_data = [
+    0,
+    1,
+    -1,
+    2,
+    43,
+    -543656,
+    423538409739,
+    0.0,
+    123.456,
+    -76.4,
+    1.0E-40,
+    1.0E40,
+    'test',
+    'TEST',
+    'Lorem Ipsum',
+    '你好',
+    'Мир!',
+    '',
+    True,
+    False,
+    None,
+    b'',
+    b'0',
+    b'123456789',
+    b'h9832y9r8wf08hw85h0h2508h0858',
+    b'\x45\xf0\xab',
+]
+
+
+def check_fetch_parameters(cursor, param, use_tuple: bool):
+    cursor.execute("select ?", (param,) if use_tuple else [param])
+    data = cursor.fetchone()
+    assert len(data) == 1
+    if isinstance(param, float):
+        assert data[0] == pytest.approx(param)
+    else:
+        assert data[0] == param
+
+
[email protected]("param", test_data)
+def test_fetch_parameter_list(cursor, param):
+    check_fetch_parameters(cursor, param, False)
+
+
[email protected]("param", test_data)
+def test_fetch_parameter_tuple(cursor, param):
+    check_fetch_parameters(cursor, param, True)
diff --git a/modules/platforms/python/tests/test_fetch_table.py 
b/modules/platforms/python/tests/test_fetch_table.py
index dd9b3d16ab..f270922248 100644
--- a/modules/platforms/python/tests/test_fetch_table.py
+++ b/modules/platforms/python/tests/test_fetch_table.py
@@ -14,16 +14,7 @@
 # limitations under the License.
 import pytest
 
-import pyignite3
-from tests.util import start_cluster_gen, check_cluster_started, 
server_addresses_basic
-
-
[email protected](autouse=True)
-def cluster():
-    if not check_cluster_started():
-        yield from start_cluster_gen()
-    else:
-        yield None
+TEST_ROWS_NUM = 15
 
 
 def create_and_populate_test_table(cursor, rows_num, table_name):
@@ -40,154 +31,127 @@ def check_row(i, row):
     assert row[2] == pytest.approx(i / 2.0)
 
 
-def test_fetchone_table_empty():
-    table_name = test_fetchone_table_empty.__name__
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            try:
-                cursor.execute(f'drop table if exists {table_name}')
-                cursor.execute(f'create table {table_name}(id int primary key, 
col1 varchar)')
-                cursor.execute(f"select col1, id from {table_name}")
-                end = cursor.fetchone()
-                assert end is None
-
-            finally:
-                cursor.execute(f'drop table if exists {table_name}')
-
-
-def test_fetchone_table_many_rows():
-    table_name = test_fetchone_table_many_rows.__name__
-    rows_num = 15
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            try:
-                create_and_populate_test_table(cursor, rows_num, table_name)
+def test_fetchone_table_empty(table_name, cursor, drop_table_cleanup):
+    cursor.execute(f'drop table if exists {table_name}')
+    cursor.execute(f'create table {table_name}(id int primary key, col1 
varchar)')
+    cursor.execute(f"select col1, id from {table_name}")
+    end = cursor.fetchone()
+    assert end is None
+
+
+def test_fetchone_table_many_rows(table_name, cursor, drop_table_cleanup):
+    create_and_populate_test_table(cursor, TEST_ROWS_NUM, table_name)
 
-                cursor.execute(f"select id, data, fl from {table_name} order 
by id")
+    cursor.execute(f"select id, data, fl from {table_name} order by id")
+
+    for i in range(TEST_ROWS_NUM):
+        row = cursor.fetchone()
+        check_row(i, row)
+
+    end = cursor.fetchone()
+    assert end is None
+
+
+def test_fetchmany_table_empty(table_name, cursor, drop_table_cleanup):
+    cursor.execute(f'drop table if exists {table_name}')
+    cursor.execute(f'create table {table_name}(id int primary key, col1 
varchar)')
+    cursor.execute(f"select col1, id from {table_name}")
+    end = cursor.fetchmany(size=10)
+    assert end is None
 
-                for i in range(rows_num):
-                    row = cursor.fetchone()
-                    check_row(i, row)
-
-                end = cursor.fetchone()
-                assert end is None
-
-            finally:
-                cursor.execute(f'drop table if exists {table_name}')
-
-
-def test_fetchmany_table_empty():
-    table_name = test_fetchmany_table_empty.__name__
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            try:
-                cursor.execute(f'drop table if exists {table_name}')
-                cursor.execute(f'create table {table_name}(id int primary key, 
col1 varchar)')
-                cursor.execute(f"select col1, id from {table_name}")
-                end = cursor.fetchmany(size=10)
-                assert end is None
-
-            finally:
-                cursor.execute(f'drop table if exists {table_name}')
-
-
-def test_fetchmany_table_many_rows():
-    table_name = test_fetchmany_table_many_rows.__name__
-    rows_num = 15
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            try:
-                create_and_populate_test_table(cursor, rows_num, table_name)
-
-                cursor.arraysize = 5
-                cursor.execute(f"select id, data, fl from {table_name} order 
by id")
-
-                rows0_4 = cursor.fetchmany()
-                assert len(rows0_4) == 5
-                for i in range(5):
-                    check_row(i, rows0_4[i])
-
-                rows5_12 = cursor.fetchmany(size=8)
-                assert len(rows5_12) == 8
-                for i in range(8):
-                    check_row(i + 5, rows5_12[i])
-
-                rows13_14 = cursor.fetchmany()
-                assert len(rows13_14) == 2
-                for i in range(2):
-                    check_row(i + 13, rows13_14[i])
-
-                end = cursor.fetchone()
-                assert end is None
-
-            finally:
-                cursor.execute(f'drop table if exists {table_name}')
-
-
-def test_fetchall_table_empty():
-    table_name = test_fetchmany_table_empty.__name__
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            try:
-                cursor.execute(f'drop table if exists {table_name}')
-                cursor.execute(f'create table {table_name}(id int primary key, 
col1 varchar)')
-                cursor.execute(f"select col1, id from {table_name}")
-                end = cursor.fetchall()
-                assert end is None
-
-            finally:
-                cursor.execute(f'drop table if exists {table_name}')
-
-
-def test_fetchall_table_many_rows():
-    table_name = test_fetchmany_table_many_rows.__name__
-    rows_num = 15
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            try:
-                create_and_populate_test_table(cursor, rows_num, table_name)
-
-                cursor.arraysize = 5
-                cursor.execute(f"select id, data, fl from {table_name} order 
by id")
 
-                rows_all = cursor.fetchall()
-                assert len(rows_all) == rows_num
-                for i in range(rows_num):
-                    check_row(i, rows_all[i])
-
-                end = cursor.fetchone()
-                assert end is None
-
-            finally:
-                cursor.execute(f'drop table if exists {table_name}')
+def test_fetchmany_table_many_rows(table_name, cursor, drop_table_cleanup):
+    create_and_populate_test_table(cursor, TEST_ROWS_NUM, table_name)
+
+    cursor.arraysize = 5
+    cursor.execute(f"select id, data, fl from {table_name} order by id")
+
+    rows0_4 = cursor.fetchmany()
+    assert len(rows0_4) == 5
+    for i in range(5):
+        check_row(i, rows0_4[i])
+
+    rows5_12 = cursor.fetchmany(size=8)
+    assert len(rows5_12) == 8
+    for i in range(8):
+        check_row(i + 5, rows5_12[i])
+
+    rows13_14 = cursor.fetchmany()
+    assert len(rows13_14) == 2
+    for i in range(2):
+        check_row(i + 13, rows13_14[i])
+
+    end = cursor.fetchone()
+    assert end is None
+
+
+def test_fetchall_table_empty(table_name, cursor, drop_table_cleanup):
+    cursor.execute(f'drop table if exists {table_name}')
+    cursor.execute(f'create table {table_name}(id int primary key, col1 
varchar)')
+    cursor.execute(f"select col1, id from {table_name}")
+    end = cursor.fetchall()
+    assert end is None
+
 
+def test_fetchall_table_many_rows(table_name, cursor, drop_table_cleanup):
+    create_and_populate_test_table(cursor, TEST_ROWS_NUM, table_name)
+
+    cursor.arraysize = 5
+    cursor.execute(f"select id, data, fl from {table_name} order by id")
+
+    rows_all = cursor.fetchall()
+    assert len(rows_all) == TEST_ROWS_NUM
+    for i in range(TEST_ROWS_NUM):
+        check_row(i, rows_all[i])
+
+    end = cursor.fetchone()
+    assert end is None
+
+
+def test_fetch_mixed_table_many_rows(table_name, cursor, drop_table_cleanup):
+    create_and_populate_test_table(cursor, TEST_ROWS_NUM, table_name)
+
+    cursor.arraysize = 4
+    cursor.execute(f"select id, data, fl from {table_name} order by id")
+
+    rows0_3 = cursor.fetchmany()
+    assert len(rows0_3) == 4
+    for i in range(4):
+        check_row(i, rows0_3[i])
+
+    row4 = cursor.fetchone()
+    check_row(4, row4)
+
+    rows_remaining = cursor.fetchall()
+    assert len(rows_remaining) == TEST_ROWS_NUM - 5
+    for i in range(TEST_ROWS_NUM - 5):
+        check_row(i + 5, rows_remaining[i])
+
+    end = cursor.fetchone()
+    assert end is None
+
+
+def test_fetchone_table_many_rows_parameter(table_name, cursor, 
drop_table_cleanup):
+    create_and_populate_test_table(cursor, TEST_ROWS_NUM, table_name)
+
+    cursor.execute(f"select id, data, fl from {table_name} where id = ? order 
by id", [13])
+
+    row = cursor.fetchone()
+    check_row(13, row)
+
+    end = cursor.fetchone()
+    assert end is None
+
+
+def test_insert_arguments_fetchone(table_name, cursor, drop_table_cleanup):
+    cursor.execute(f'create table {table_name}(id int primary key, data 
varchar, fl double)')
+    for i in range(TEST_ROWS_NUM):
+        cursor.execute(f"insert into {table_name} values (?, ?, ?)", [i, 
f'Value-{i * 2}', i / 2.0])
 
-def test_fetch_mixed_table_many_rows():
-    table_name = test_fetch_mixed_table_many_rows.__name__
-    rows_num = 15
-    with pyignite3.connect(address=server_addresses_basic[0]) as conn:
-        with conn.cursor() as cursor:
-            try:
-                create_and_populate_test_table(cursor, rows_num, table_name)
-
-                cursor.arraysize = 5
-                cursor.execute(f"select id, data, fl from {table_name} order 
by id")
-
-                rows0_4 = cursor.fetchmany()
-                assert len(rows0_4) == 5
-                for i in range(5):
-                    check_row(i, rows0_4[i])
-
-                row5 = cursor.fetchone()
-                check_row(5, row5)
-
-                rows_remaining = cursor.fetchall()
-                assert len(rows_remaining) == rows_num - 6
-                for i in range(rows_num - 6):
-                    check_row(i + 6, rows_remaining[i])
+    cursor.execute(f"select id, data, fl from {table_name} where id = ?", [3])
 
-                end = cursor.fetchone()
-                assert end is None
+    row = cursor.fetchone()
+    check_row(3, row)
 
-            finally:
-                cursor.execute(f'drop table if exists {table_name}')
+    end = cursor.fetchone()
+    assert end is None
diff --git a/modules/platforms/python/tests/util.py 
b/modules/platforms/python/tests/util.py
index d26b4e5edc..ccd856d87d 100644
--- a/modules/platforms/python/tests/util.py
+++ b/modules/platforms/python/tests/util.py
@@ -92,7 +92,7 @@ def kill_process_tree(pid):
 # noinspection PyBroadException
 def check_server_started(addr: str) -> bool:
     try:
-        conn = pyignite3.connect(address=addr, timeout=1)
+        conn = pyignite3.connect(address=[addr], timeout=1)
     except RuntimeError as e:
         return False
 

Reply via email to