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 4db36abc10 IGNITE-22740 DB API Driver 3: Implement Transactions (#4485)
4db36abc10 is described below

commit 4db36abc102999c97b29d14d0f95ad79bfc91801
Author: Igor Sapego <[email protected]>
AuthorDate: Tue Oct 1 11:22:30 2024 +0200

    IGNITE-22740 DB API Driver 3: Implement Transactions (#4485)
---
 .../platforms/cpp/ignite/odbc/sql_connection.cpp   |  13 +-
 modules/platforms/python/cpp_module/module.cpp     |  14 +-
 .../platforms/python/cpp_module/py_connection.cpp  |  83 +++++++-
 modules/platforms/python/cpp_module/py_cursor.cpp  |  18 +-
 .../platforms/python/cpp_module/type_conversion.h  |   3 +-
 modules/platforms/python/pyignite3/__init__.py     | 213 +++++++++++++++++++--
 modules/platforms/python/tests/conftest.py         |   1 +
 modules/platforms/python/tests/test_connect.py     |   9 +
 modules/platforms/python/tests/test_fetch_table.py |  17 ++
 .../platforms/python/tests/test_transactions.py    |  80 ++++++++
 10 files changed, 412 insertions(+), 39 deletions(-)

diff --git a/modules/platforms/cpp/ignite/odbc/sql_connection.cpp 
b/modules/platforms/cpp/ignite/odbc/sql_connection.cpp
index 0c8c69e6e9..0e3b08b053 100644
--- a/modules/platforms/cpp/ignite/odbc/sql_connection.cpp
+++ b/modules/platforms/cpp/ignite/odbc/sql_connection.cpp
@@ -445,6 +445,7 @@ void sql_connection::transaction_start() {
 
 sql_result sql_connection::enable_autocommit() {
     assert(!m_auto_commit);
+    LOG_MSG("m_transaction_id: " << (m_transaction_id.has_value() ? 
*m_transaction_id : -1));
 
     if (m_transaction_id) {
         sql_result res;
@@ -585,11 +586,15 @@ sql_result sql_connection::internal_set_attribute(int 
attr, void *value, SQLINTE
             }
 
             auto autocommit_now = mode == SQL_AUTOCOMMIT_ON;
+            LOG_MSG("autocommit current: " << m_auto_commit << ", autocommit 
to set: " << autocommit_now);
 
-            if (autocommit_now && !m_auto_commit)
-                return enable_autocommit();
-            else
-                return disable_autocommit();
+            if (autocommit_now != m_auto_commit) {
+                if (autocommit_now)
+                    return enable_autocommit();
+                else
+                    return disable_autocommit();
+            }
+            return sql_result::AI_SUCCESS;
         }
 
         default: {
diff --git a/modules/platforms/python/cpp_module/module.cpp 
b/modules/platforms/python/cpp_module/module.cpp
index f62da75ce9..19a3e588ad 100644
--- a/modules/platforms/python/cpp_module/module.cpp
+++ b/modules/platforms/python/cpp_module/module.cpp
@@ -65,6 +65,7 @@ static PyObject* pyignite3_connect(PyObject* self, PyObject* 
args, PyObject* kwa
         "timezone",
         "page_size",
         "timeout",
+        "autocommit",
         nullptr
     };
 
@@ -75,9 +76,10 @@ static PyObject* pyignite3_connect(PyObject* self, PyObject* 
args, PyObject* kwa
     const char *timezone = nullptr;
     int timeout = 0;
     int page_size = 0;
+    int autocommit = 1;
 
-    int parsed = PyArg_ParseTupleAndKeywords(
-        args, kwargs, "O|$ssssii", kwlist, &address, &identity, &secret, 
&schema, &timezone, &timeout, &page_size);
+    int parsed = PyArg_ParseTupleAndKeywords(args, kwargs, "O|$ssssiip", 
kwlist,
+        &address, &identity, &secret, &schema, &timezone, &timeout, 
&page_size, &autocommit);
 
     if (!parsed)
         return nullptr;
@@ -153,6 +155,14 @@ static PyObject* pyignite3_connect(PyObject* self, 
PyObject* args, PyObject* kwa
     if (!check_errors(*sql_conn))
         return nullptr;
 
+    if (!autocommit)
+    {
+        void* ptr_autocommit = (void*)(ptrdiff_t(SQL_AUTOCOMMIT_OFF));
+        sql_conn->set_attribute(SQL_ATTR_AUTOCOMMIT, ptr_autocommit, 0);
+        if (!check_errors(*sql_conn))
+            return nullptr;
+    }
+
     return make_connection(std::move(sql_env), std::move(sql_conn));
 }
 
diff --git a/modules/platforms/python/cpp_module/py_connection.cpp 
b/modules/platforms/python/cpp_module/py_connection.cpp
index 55bd5c9a0f..6cc08f64ab 100644
--- a/modules/platforms/python/cpp_module/py_connection.cpp
+++ b/modules/platforms/python/cpp_module/py_connection.cpp
@@ -25,6 +25,20 @@
 
 #include <Python.h>
 
+/**
+ * Check if the connection is open. Set error if not.
+ *
+ * @param self Connection.
+ * @return @c true if open and @c false otherwise.
+ */
+static bool py_connection_expect_open(py_connection* self) {
+    if (!self->m_connection) {
+        PyErr_SetString(py_get_module_interface_error_class(), "Connection is 
in invalid state (Already closed?)");
+        return false;
+    }
+    return true;
+}
+
 int py_connection_init(py_connection *self, PyObject *args, PyObject *kwds)
 {
     UNUSED_VALUE args;
@@ -61,8 +75,7 @@ static PyObject* py_connection_close(py_connection* self, 
PyObject*)
         self->m_environment = nullptr;
     }
 
-    Py_INCREF(Py_None);
-    return Py_None;
+    Py_RETURN_NONE;
 }
 
 static PyObject* py_connection_cursor(py_connection* self, PyObject*)
@@ -81,8 +94,66 @@ static PyObject* py_connection_cursor(py_connection* self, 
PyObject*)
         return py_cursor_obj;
     }
 
-    Py_INCREF(Py_None);
-    return Py_None;
+    Py_RETURN_NONE;
+}
+
+static PyObject* py_connection_autocommit(py_connection* self, PyObject*)
+{
+    if (!py_connection_expect_open(self))
+        return nullptr;
+
+    SQLUINTEGER res = 0;
+    self->m_connection->get_attribute(SQL_ATTR_AUTOCOMMIT, &res, 0, nullptr);
+    if (!check_errors(*self->m_connection))
+        return nullptr;
+
+    if (!res) {
+        Py_RETURN_FALSE;
+    }
+
+    Py_RETURN_TRUE;
+}
+
+static PyObject* py_connection_set_autocommit(py_connection* self, PyObject* 
value)
+{
+    if (!py_connection_expect_open(self))
+        return nullptr;
+
+    if (!PyBool_Check(value)) {
+        PyErr_SetString(py_get_module_interface_error_class(), "Autocommit 
attribute should be of a type bool");
+        return nullptr;
+    }
+
+    void* ptr_autocommit = (void*)(ptrdiff_t((value == Py_True) ? 
SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF));
+    self->m_connection->set_attribute(SQL_ATTR_AUTOCOMMIT, ptr_autocommit, 0);
+    if (!check_errors(*self->m_connection))
+        return nullptr;
+
+    Py_RETURN_NONE;
+}
+
+static PyObject* py_connection_commit(py_connection* self, PyObject*)
+{
+    if (!py_connection_expect_open(self))
+        return nullptr;
+
+    self->m_connection->transaction_commit();
+    if (!check_errors(*self->m_connection))
+        return nullptr;
+
+    Py_RETURN_NONE;
+}
+
+static PyObject* py_connection_rollback(py_connection* self, PyObject*)
+{
+    if (!py_connection_expect_open(self))
+        return nullptr;
+
+    self->m_connection->transaction_rollback();
+    if (!check_errors(*self->m_connection))
+        return nullptr;
+
+    Py_RETURN_NONE;
 }
 
 static PyTypeObject py_connection_type = {
@@ -93,6 +164,10 @@ static PyTypeObject py_connection_type = {
 static struct PyMethodDef py_connection_methods[] = {
     {"close", (PyCFunction)py_connection_close, METH_NOARGS, nullptr},
     {"cursor", (PyCFunction)py_connection_cursor, METH_NOARGS, nullptr},
+    {"autocommit", (PyCFunction)py_connection_autocommit, METH_NOARGS, 
nullptr},
+    {"set_autocommit", (PyCFunction)py_connection_set_autocommit, METH_O, 
nullptr},
+    {"commit", (PyCFunction)py_connection_commit, METH_NOARGS, nullptr},
+    {"rollback", (PyCFunction)py_connection_rollback, METH_NOARGS, nullptr},
     {nullptr, nullptr, 0, nullptr}
 };
 
diff --git a/modules/platforms/python/cpp_module/py_cursor.cpp 
b/modules/platforms/python/cpp_module/py_cursor.cpp
index a5b74e35cc..80801e34b5 100644
--- a/modules/platforms/python/cpp_module/py_cursor.cpp
+++ b/modules/platforms/python/cpp_module/py_cursor.cpp
@@ -159,8 +159,7 @@ static PyObject* py_cursor_close(py_cursor* self, PyObject*)
         self->m_statement = nullptr;
     }
 
-    Py_INCREF(Py_None);
-    return Py_None;
+    Py_RETURN_NONE;
 }
 
 static PyObject* py_cursor_execute(py_cursor* self, PyObject* args, PyObject* 
kwargs)
@@ -205,8 +204,7 @@ static PyObject* py_cursor_execute(py_cursor* self, 
PyObject* args, PyObject* kw
     if (!check_errors(*self->m_statement))
         return nullptr;
 
-    Py_INCREF(Py_None);
-    return Py_None;
+    Py_RETURN_NONE;
 }
 
 static PyObject* py_cursor_rowcount(py_cursor* self, PyObject*)
@@ -240,15 +238,13 @@ static PyObject* py_cursor_fetchone(py_cursor* self, 
PyObject*)
     }
 
     if (!query->is_data_available()) {
-        Py_INCREF(Py_None);
-        return Py_None;
+        Py_RETURN_NONE;
     }
 
     auto& query0 = static_cast<ignite::data_query&>(*query);
     auto res = query0.fetch_next_row();
     if (res == ignite::sql_result::AI_NO_DATA) {
-        Py_INCREF(Py_None);
-        return Py_None;
+        Py_RETURN_NONE;
     }
 
     if (!check_errors(*self->m_statement)) {
@@ -354,8 +350,7 @@ static PyObject* py_cursor_column_display_size(py_cursor* 
self, PyObject*)
     if (!py_cursor_expect_open(self))
         return nullptr;
 
-    Py_INCREF(Py_None);
-    return Py_None;
+    Py_RETURN_NONE;
 }
 
 static PyObject* py_cursor_column_internal_size(py_cursor* self, PyObject*)
@@ -363,8 +358,7 @@ static PyObject* py_cursor_column_internal_size(py_cursor* 
self, PyObject*)
     if (!py_cursor_expect_open(self))
         return nullptr;
 
-    Py_INCREF(Py_None);
-    return Py_None;
+    Py_RETURN_NONE;
 }
 
 static PyObject* py_cursor_column_precision(py_cursor* self, PyObject* args)
diff --git a/modules/platforms/python/cpp_module/type_conversion.h 
b/modules/platforms/python/cpp_module/type_conversion.h
index c1bf3d42db..a2c743b3e2 100644
--- a/modules/platforms/python/cpp_module/type_conversion.h
+++ b/modules/platforms/python/cpp_module/type_conversion.h
@@ -38,8 +38,7 @@ static PyObject* primitive_to_pyobject(ignite::primitive 
value) {
     using ignite::ignite_type;
 
     if (value.is_null()) {
-        Py_INCREF(Py_None);
-        return Py_None;
+        Py_RETURN_NONE;
     }
 
     switch (value.get_type()) {
diff --git a/modules/platforms/python/pyignite3/__init__.py 
b/modules/platforms/python/pyignite3/__init__.py
index 5c2a172c6e..3b60816f44 100644
--- a/modules/platforms/python/pyignite3/__init__.py
+++ b/modules/platforms/python/pyignite3/__init__.py
@@ -31,21 +31,50 @@ threadsafety = 1
 # Parameter style is a question mark, e.g. '...WHERE name=?'
 paramstyle = 'qmark'
 
-NIL = None
+# Null constant
+NULL = None
+
+# Boolean type
 BOOLEAN = bool
+
+# Integer type
 INT = int
+
+# Floating point type
 FLOAT = float
+
+# String type
 STRING = str
+
+# Binary type
 BINARY = bytes
+
+# Big number (Decimal) type
 NUMBER = decimal.Decimal
+
+# Date type
 DATE = datetime.date
+
+# Time type
 TIME = datetime.time
+
+# Date-Time type
 DATETIME = datetime.datetime
+
+# Duration type
 DURATION = datetime.timedelta
+
+# UUID type
 UUID = uuid.UUID
 
+# This type object is used to describe the “Row ID” column in a database.
+ROWID = UUID
+
 
 class TIMESTAMP(float):
+    """
+    Timestamp data type.
+    """
     pass
 
 
@@ -112,7 +141,7 @@ def Binary(string: str):
 
 def _type_code_from_int(native: int):
     if native == native_type_code.NIL:
-        return NIL
+        return NULL
     elif native == native_type_code.BOOLEAN:
         return BOOLEAN
     elif (native == native_type_code.INT8 or native == native_type_code.INT16
@@ -142,6 +171,10 @@ def _type_code_from_int(native: int):
 
 
 class ColumnDescription:
+    """
+    Represents a description of the single column of the result set.
+    """
+
     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
@@ -165,9 +198,11 @@ class Cursor:
     """
     arraysize: int = 1
 
-    def __init__(self, py_cursor):
+    def __init__(self, py_cursor, conn):
         self._py_cursor = py_cursor
         self._description = None
+        self._rownumber = None
+        self._conn = conn
 
     def __enter__(self):
         return self
@@ -175,6 +210,28 @@ class Cursor:
     def __exit__(self, exc_type, exc_val, exc_tb):
         self.close()
 
+    def __iter__(self):
+        """
+        Return self to make cursors compatible to the iteration protocol
+        """
+        return self
+
+    def __next__(self) -> Sequence[Optional[Any]]:
+        """
+        Return the next row to make cursors compatible to the iteration 
protocol.
+        """
+        return self.next()
+
+    def next(self) -> Sequence[Optional[Any]]:
+        """
+        Return the next row from the currently executing SQL statement using 
the same semantics as .fetchone().
+        A StopIteration exception is raised when the result set is exhausted.
+        """
+        res = self.fetchone()
+        if res is None:
+            raise StopIteration
+        return res
+
     @property
     def description(self) -> Optional[List[ColumnDescription]]:
         """
@@ -199,8 +256,8 @@ class Cursor:
     @property
     def rowcount(self) -> int:
         """
-        This read-only attribute specifies the number of rows that the last 
.execute*() produced
-        (for DQL statements like SELECT) or affected (for DML statements like 
UPDATE or INSERT).
+        This read-only attribute specifies the number of rows that the last 
.execute*() produced (for DQL statements
+        like SELECT) or affected (for DML statements like UPDATE or INSERT).
         The attribute is -1 in case no .execute*() has been performed on the 
cursor or the rowcount of the last
         operation is cannot be determined by the interface.
         """
@@ -208,6 +265,31 @@ class Cursor:
             return -1
         return self._py_cursor.rowcount()
 
+    @property
+    def rownumber(self) -> Optional[int]:
+        """
+        This read-only attribute provides the current 0-based index of the 
cursor in the result set or None if the index
+        cannot be determined.
+        The index can be seen as index of the cursor in a sequence (the result 
set). The next fetch operation will fetch
+        the row indexed by .rownumber in that sequence.
+        """
+        return self._rownumber
+
+    @property
+    def connection(self):
+        """
+        This read-only attribute return a reference to the Connection object 
on which the cursor was created.
+        """
+        return self._conn
+
+    @property
+    def lastrowid(self):
+        """
+        This read-only attribute provides the rowid of the last modified row 
(most databases return a rowid only when a
+        single INSERT operation is performed). As Ignite does not support 
rowids, this attribute is always set to None.
+        """
+        return None
+
     def callproc(self, *_args):
         if self._py_cursor is None:
             raise InterfaceError('Connection is already closed')
@@ -222,6 +304,7 @@ class Cursor:
         if self._py_cursor is not None:
             self._py_cursor.close()
             self._py_cursor = None
+            self._rownumber = None
 
     def execute(self, query: str, params: Optional[Union[List[Any], 
Tuple[Any]]] = None):
         """
@@ -238,6 +321,7 @@ class Cursor:
 
         self._py_cursor.execute(query, params)
         self._update_description()
+        self._rownumber = 0
 
     def _update_description(self):
         """
@@ -272,7 +356,13 @@ class Cursor:
         if self._py_cursor is None:
             raise InterfaceError('Connection is already closed')
 
-        return self._py_cursor.fetchone()
+        res = self._py_cursor.fetchone()
+        if res is None:
+            self._rownumber = None
+        else:
+            self._rownumber += 1
+
+        return res
 
     def fetchmany(self, size: Optional[int] = None) -> 
Optional[Sequence[Sequence[Optional[Any]]]]:
         """
@@ -349,6 +439,7 @@ class Connection:
     """
 
     def __init__(self):
+        self._autocommit = True
         self._py_connection = None
 
     def __enter__(self):
@@ -360,6 +451,7 @@ class Connection:
     def close(self):
         """
         Close active connection.
+        Closing a connection without committing the changes first will cause 
an implicit rollback to be performed.
         Completes without errors on successfully closed connections.
         """
         if self._py_connection is not None:
@@ -367,23 +459,77 @@ class Connection:
             self._py_connection = None
 
     def commit(self):
+        """
+        Commit any pending transaction to the database.
+        """
         if self._py_connection is None:
             raise InterfaceError('Connection is already closed')
 
-        # TODO: IGNITE-22740 Implement transaction support
-        raise NotSupportedError('Transactions are not supported')
+        self._py_connection.commit()
 
     def rollback(self):
+        """
+        Roll back to the start of any pending transaction.
+        Closing a connection without committing the changes first will cause 
an implicit rollback to be performed.
+        """
         if self._py_connection is None:
             raise InterfaceError('Connection is already closed')
 
-        # TODO: IGNITE-22740 Implement transaction support
-        raise NotSupportedError('Transactions are not supported')
+        self._py_connection.rollback()
+
+    @property
+    def autocommit(self) -> bool:
+        """
+        Attribute to query and set the autocommit mode of the connection.
+        Return True if the connection is operating in autocommit 
(non-transactional) mode. Return False if
+        the connection is operating in manual commit (transactional) mode.
+
+        Setting the attribute to True or False adjusts the connection’s mode 
accordingly.
+
+        Changing the setting from True to False (disabling autocommit) will 
have the database leave autocommit mode
+        and start a new transaction.
+
+        Changing from False to True (enabling autocommit) has database 
dependent semantics with respect to how pending
+        transactions are handled.
+        """
+        if self._py_connection is None:
+            return True
+        return self._py_connection.autocommit()
+
+    @autocommit.setter
+    def autocommit(self, value):
+        """
+        Attribute to query and set the autocommit mode of the connection.
+        Setting the attribute to True or False adjusts the connection’s mode 
accordingly.
+
+        Changing the setting from True to False (disabling autocommit) will 
have the database leave autocommit mode
+        and start a new transaction.
+
+        Changing from False to True (enabling autocommit) has database 
dependent semantics with respect to how pending
+        transactions are handled.
+        """
+        self.setautocommit(value)
+
+    def setautocommit(self, value: bool):
+        """
+        Set the autocommit mode of the connection. Adjusts the connection’s 
mode accordingly.
+
+        Changing the setting from True to False (disabling autocommit) will 
have the database leave autocommit mode
+        and start a new transaction.
+
+        Changing from False to True (enabling autocommit) has database 
dependent semantics with respect to how pending
+        transactions are handled.
+        """
+        if self._py_connection is not None:
+            self._py_connection.set_autocommit(value)
 
     def cursor(self) -> Cursor:
+        """
+        Return a new Cursor Object using the connection.
+        """
         if self._py_connection is None:
             raise InterfaceError('Connection is already closed')
-        return Cursor(self._py_connection.cursor())
+        return Cursor(py_cursor=self._py_connection.cursor(), conn=self)
 
 
 def connect(address: [str], **kwargs) -> Connection:
@@ -410,46 +556,83 @@ def connect(address: [str], **kwargs) -> Connection:
         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.
+    autocommit: bool, optional
+        The autocommit mode of the connection. Default value: True.
     """
     return _pyignite3_extension.connect(address=address, **kwargs)
 
 
 class Error(Exception):
+    """
+    Exception that is the base class of all other error exceptions. You can 
use this to catch all errors with one single
+    except statement. Warnings are not considered errors and thus should not 
use this class as base.
+    """
     pass
 
 
 # noinspection PyShadowingBuiltins
 class Warning(Exception):
+    """
+    Exception raised for important warnings like data truncations while 
inserting, etc.
+    """
     pass
 
 
 class InterfaceError(Error):
+    """
+    Exception raised for errors that are related to the database interface 
rather than the database itself.
+    """
     pass
 
 
 class DatabaseError(Error):
+    """
+    Exception raised for errors that are related to the database.
+    """
+    pass
+
+
+class DataError(DatabaseError):
+    """
+    Exception raised for errors that are due to problems with the processed 
data like division by zero, numeric value
+    out of range, etc..
+    """
     pass
 
 
 class InternalError(DatabaseError):
+    """
+    Exception raised when the relational integrity of the database is 
affected, e.g. a foreign key check fails.
+    """
     pass
 
 
 class OperationalError(DatabaseError):
+    """
+    Exception raised for errors that are related to the database’s operation 
and not necessarily under the control of
+    the programmer, e.g. an unexpected disconnect occurs, the data source name 
is not found, a transaction could not be
+    processed, a memory allocation error occurred during processing, etc.
+    """
     pass
 
 
 class ProgrammingError(DatabaseError):
+    """
+    Exception raised for programming errors, e.g. table not found or already 
exists, syntax error in the SQL statement,
+    wrong number of parameters specified, etc.
+    """
     pass
 
 
 class IntegrityError(DatabaseError):
-    pass
-
-
-class DataError(DatabaseError):
+    """
+    Exception raised when the relational integrity of the database is 
affected, e.g. a foreign key check fails.
+    """
     pass
 
 
 class NotSupportedError(DatabaseError):
+    """
+    Exception raised in case a method or database API was used which is not 
supported by the database.
+    """
     pass
diff --git a/modules/platforms/python/tests/conftest.py 
b/modules/platforms/python/tests/conftest.py
index bbacc5b7e0..1d3c1efe59 100644
--- a/modules/platforms/python/tests/conftest.py
+++ b/modules/platforms/python/tests/conftest.py
@@ -45,6 +45,7 @@ def cursor(connection):
 @pytest.fixture()
 def drop_table_cleanup(cursor, table_name):
     yield None
+    cursor.connection.setautocommit(True)
     cursor.execute(f'drop table if exists {table_name}')
 
 
diff --git a/modules/platforms/python/tests/test_connect.py 
b/modules/platforms/python/tests/test_connect.py
index 34ccc74bc8..25b5805abd 100644
--- a/modules/platforms/python/tests/test_connect.py
+++ b/modules/platforms/python/tests/test_connect.py
@@ -24,6 +24,15 @@ def test_connection_success():
     conn.close()
 
 
+def test_connection_get_cursor():
+    with pyignite3.connect(address=server_addresses_basic, timeout=1) as conn:
+        assert conn is not None
+
+        cursor = conn.cursor()
+        assert cursor.connection is conn
+        cursor.close()
+
+
 def test_connection_fail():
     with pytest.raises(pyignite3.OperationalError) as err:
         pyignite3.connect(address=server_addresses_invalid, timeout=1)
diff --git a/modules/platforms/python/tests/test_fetch_table.py 
b/modules/platforms/python/tests/test_fetch_table.py
index f270922248..fb190f4ff3 100644
--- a/modules/platforms/python/tests/test_fetch_table.py
+++ b/modules/platforms/python/tests/test_fetch_table.py
@@ -114,19 +114,27 @@ def test_fetch_mixed_table_many_rows(table_name, cursor, 
drop_table_cleanup):
     cursor.arraysize = 4
     cursor.execute(f"select id, data, fl from {table_name} order by id")
 
+    assert cursor.rownumber == 0
+
     rows0_3 = cursor.fetchmany()
     assert len(rows0_3) == 4
     for i in range(4):
         check_row(i, rows0_3[i])
 
+    assert cursor.rownumber == 4
+
     row4 = cursor.fetchone()
     check_row(4, row4)
 
+    assert cursor.rownumber == 5
+
     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])
 
+    assert cursor.rownumber is None
+
     end = cursor.fetchone()
     assert end is None
 
@@ -155,3 +163,12 @@ def test_insert_arguments_fetchone(table_name, cursor, 
drop_table_cleanup):
 
     end = cursor.fetchone()
     assert end is None
+
+
+def test_cursor_iterable(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")
+
+    for i, row in enumerate(cursor):
+        check_row(i, row)
diff --git a/modules/platforms/python/tests/test_transactions.py 
b/modules/platforms/python/tests/test_transactions.py
new file mode 100644
index 0000000000..280b9da90f
--- /dev/null
+++ b/modules/platforms/python/tests/test_transactions.py
@@ -0,0 +1,80 @@
+# 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 pyignite3
+from tests.util import server_addresses_basic
+
+
+def create_tx_test_table(cursor, table_name):
+    cursor.execute(f'drop table if exists {table_name}')
+    cursor.execute(f'create table {table_name}(id int primary key, val int)')
+
+
+def test_commit_rollback_setautocommit(table_name, connection, cursor, 
drop_table_cleanup):
+    create_tx_test_table(cursor, table_name)
+    assert connection.autocommit is True
+
+    connection.setautocommit(False)
+    assert connection.autocommit is False
+
+    cursor.execute(f'insert into {table_name} values (42, 10)')
+    connection.commit()
+
+    cursor.execute(f'update {table_name} set val=23 where id=42')
+    connection.rollback()
+
+    cursor.execute(f'select val from {table_name} where id=42')
+    row = cursor.fetchone()
+
+    assert row[0] == 10
+
+
+def test_commit_rollback_autocommit_setter(table_name, connection, cursor, 
drop_table_cleanup):
+    create_tx_test_table(cursor, table_name)
+    assert connection.autocommit is True
+
+    cursor.execute(f'insert into {table_name} values (42, 10)')
+
+    connection.autocommit = False
+    assert connection.autocommit is False
+
+    cursor.execute(f'update {table_name} set val=23 where id=42')
+    connection.rollback()
+
+    cursor.execute(f'select val from {table_name} where id=42')
+    row = cursor.fetchone()
+
+    assert row[0] == 10
+
+
+def test_commit_rollback_autocommit_connection(table_name, drop_table_cleanup):
+    with pyignite3.connect(address=server_addresses_basic, autocommit=True) as 
conn:
+        with conn.cursor() as cursor:
+            create_tx_test_table(cursor, table_name)
+
+    with pyignite3.connect(address=server_addresses_basic, autocommit=False) 
as conn_tx:
+        assert conn_tx.autocommit is False
+        with conn_tx.cursor() as cursor:
+            cursor.execute(f'insert into {table_name} values (123, 999)')
+            conn_tx.commit()
+
+            cursor.execute(f'update {table_name} set val=777 where id=123')
+
+    with pyignite3.connect(address=server_addresses_basic, autocommit=True) as 
conn:
+        assert conn.autocommit is True
+        with conn.cursor() as cursor:
+            cursor.execute(f'select val from {table_name} where id=123')
+            row = cursor.fetchone()
+            assert row[0] == 999
+

Reply via email to