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
+