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 d47db94afa IGNITE-22469 DB API Driver: Implement simple query 
execution (#4252)
d47db94afa is described below

commit d47db94afa89835aeef17f3cf8b0f47308b9697b
Author: Igor Sapego <[email protected]>
AuthorDate: Tue Aug 20 18:32:28 2024 +0400

    IGNITE-22469 DB API Driver: Implement simple query execution (#4252)
---
 .../platforms/cpp/ignite/network/CMakeLists.txt    |   7 +-
 .../platforms/cpp/ignite/odbc/sql_connection.cpp   |   1 +
 modules/platforms/cpp/ignite/odbc/sql_statement.h  |   7 +
 modules/platforms/python/cpp_module/CMakeLists.txt |   3 +-
 modules/platforms/python/cpp_module/module.cpp     |   5 +-
 .../platforms/python/cpp_module/py_connection.cpp  |  62 ++--
 .../platforms/python/cpp_module/py_connection.h    |   4 +-
 modules/platforms/python/cpp_module/py_cursor.cpp  | 313 +++++++++++++++++++++
 .../cpp_module/{py_connection.h => py_cursor.h}    |  36 +--
 modules/platforms/python/pyignite3/__init__.py     | 211 ++++++++++++--
 .../native_type_code.py}                           |  45 ++-
 modules/platforms/python/tests/test_connect.py     |  11 +-
 modules/platforms/python/tests/test_execute.py     |  99 +++++++
 modules/platforms/python/tests/util.py             |  20 +-
 14 files changed, 727 insertions(+), 97 deletions(-)

diff --git a/modules/platforms/cpp/ignite/network/CMakeLists.txt 
b/modules/platforms/cpp/ignite/network/CMakeLists.txt
index bb6116128d..3bacf2925b 100644
--- a/modules/platforms/cpp/ignite/network/CMakeLists.txt
+++ b/modules/platforms/cpp/ignite/network/CMakeLists.txt
@@ -19,7 +19,12 @@ project(ignite-network)
 
 set(TARGET ${PROJECT_NAME})
 
-find_package(OpenSSL REQUIRED)
+find_package(OpenSSL)
+if (EXISTS ${OPENSSL_INCLUDE_DIR})
+    message(STATUS "OPENSSL_INCLUDE_DIR: " ${OPENSSL_INCLUDE_DIR})
+else()
+    message(FATAL_ERROR "Can not resolve OPENSSL_INCLUDE_DIR.")
+endif()
 
 set(SOURCES
     async_client_pool_adapter.cpp
diff --git a/modules/platforms/cpp/ignite/odbc/sql_connection.cpp 
b/modules/platforms/cpp/ignite/odbc/sql_connection.cpp
index 55325aff55..0c8c69e6e9 100644
--- a/modules/platforms/cpp/ignite/odbc/sql_connection.cpp
+++ b/modules/platforms/cpp/ignite/odbc/sql_connection.cpp
@@ -727,6 +727,7 @@ bool sql_connection::try_restore_connection() {
 
 bool sql_connection::safe_connect(const end_point &addr) {
     try {
+        LOG_MSG("Connecting to " << addr.to_string());
         return m_socket->connect(addr.host.c_str(), addr.port, 
m_login_timeout);
     } catch (const ignite_error &err) {
         std::stringstream msgs;
diff --git a/modules/platforms/cpp/ignite/odbc/sql_statement.h 
b/modules/platforms/cpp/ignite/odbc/sql_statement.h
index 42a31715dd..3eeba5255f 100644
--- a/modules/platforms/cpp/ignite/odbc/sql_statement.h
+++ b/modules/platforms/cpp/ignite/odbc/sql_statement.h
@@ -337,6 +337,13 @@ public:
     void describe_param(std::uint16_t param_num, std::int16_t *data_type, 
SQLULEN *param_size,
         std::int16_t *decimal_digits, std::int16_t *nullable);
 
+    /**
+     * Get a pointer to the current query.
+     *
+     * @return Current query.
+     */
+    [[nodiscard]] query *get_query() { return m_current_query.get(); }
+
 private:
     /**
      * Bind result column to specified data buffer.
diff --git a/modules/platforms/python/cpp_module/CMakeLists.txt 
b/modules/platforms/python/cpp_module/CMakeLists.txt
index 1df497efb8..4c08dcb220 100644
--- a/modules/platforms/python/cpp_module/CMakeLists.txt
+++ b/modules/platforms/python/cpp_module/CMakeLists.txt
@@ -22,8 +22,9 @@ set(TARGET ${PROJECT_NAME})
 find_package(ODBC REQUIRED)
 
 set(SOURCES
-    py_connection.cpp
     module.cpp
+    py_connection.cpp
+    py_cursor.cpp
 )
 
 set(LIBRARIES
diff --git a/modules/platforms/python/cpp_module/module.cpp 
b/modules/platforms/python/cpp_module/module.cpp
index 4b7bd1abac..1a068b0420 100644
--- a/modules/platforms/python/cpp_module/module.cpp
+++ b/modules/platforms/python/cpp_module/module.cpp
@@ -17,6 +17,7 @@
 
 #include "module.h"
 #include "py_connection.h"
+#include "py_cursor.h"
 
 #include <ignite/odbc/sql_environment.h>
 #include <ignite/odbc/sql_connection.h>
@@ -53,10 +54,10 @@ PyMODINIT_FUNC PyInit__pyignite3_extension(void) { // 
NOLINT(*-reserved-identifi
     if (mod == nullptr)
         return nullptr;
 
-    if (prepare_py_connection_type())
+    if (prepare_py_connection_type() || prepare_py_cursor_type())
         return nullptr;
 
-    if (register_py_connection_type(mod))
+    if (register_py_connection_type(mod) || register_py_cursor_type(mod))
         return nullptr;
 
     return mod;
diff --git a/modules/platforms/python/cpp_module/py_connection.cpp 
b/modules/platforms/python/cpp_module/py_connection.cpp
index 96bb5f1259..2dd6bd82b8 100644
--- a/modules/platforms/python/cpp_module/py_connection.cpp
+++ b/modules/platforms/python/cpp_module/py_connection.cpp
@@ -17,11 +17,13 @@
 
 #include <ignite/odbc/sql_environment.h>
 #include <ignite/odbc/sql_connection.h>
+#include <ignite/odbc/sql_statement.h>
 
 #include <ignite/common/detail/config.h>
 
 #include "module.h"
 #include "py_connection.h"
+#include "py_cursor.h"
 
 #include <Python.h>
 
@@ -30,35 +32,55 @@ int py_connection_init(py_connection *self, PyObject *args, 
PyObject *kwds)
     UNUSED_VALUE args;
     UNUSED_VALUE kwds;
 
-    self->m_env = nullptr;
-    self->m_conn = nullptr;
+    self->m_environment = nullptr;
+    self->m_connection = nullptr;
 
     return 0;
 }
 
 void py_connection_dealloc(py_connection *self)
 {
-    delete self->m_conn;
-    delete self->m_env;
+    delete self->m_connection;
+    delete self->m_environment;
 
-    self->m_conn = nullptr;
-    self->m_env = nullptr;
+    self->m_connection = nullptr;
+    self->m_environment = nullptr;
 
     Py_TYPE(self)->tp_free(self);
 }
 
 static PyObject* py_connection_close(py_connection* self, PyObject*)
 {
-    if (self->m_conn) {
-        self->m_conn->release();
-        if (!check_errors(*self->m_conn))
+    if (self->m_connection) {
+        self->m_connection->release();
+        if (!check_errors(*self->m_connection))
             return nullptr;
 
-        delete self->m_conn;
-        self->m_conn = nullptr;
+        delete self->m_connection;
+        self->m_connection = nullptr;
 
-        delete self->m_env;
-        self->m_env = nullptr;
+        delete self->m_environment;
+        self->m_environment = nullptr;
+    }
+
+    Py_INCREF(Py_None);
+    return Py_None;
+}
+
+static PyObject* py_connection_cursor(py_connection* self, PyObject*)
+{
+    if (self->m_connection) {
+        std::unique_ptr<ignite::sql_statement> 
statement{self->m_connection->create_statement()};
+        if (!check_errors(*self->m_connection))
+            return nullptr;
+
+        auto py_cursor = make_py_cursor(std::move(statement));
+        if (!py_cursor)
+            return nullptr;
+
+        auto py_cursor_obj = (PyObject*)py_cursor;
+        Py_INCREF(py_cursor_obj);
+        return py_cursor_obj;
     }
 
     Py_INCREF(Py_None);
@@ -72,6 +94,7 @@ 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},
     {nullptr, nullptr, 0, nullptr}
 };
 
@@ -92,18 +115,13 @@ int register_py_connection_type(PyObject* mod) {
 
 py_connection *make_py_connection(std::unique_ptr<ignite::sql_environment> env,
     std::unique_ptr<ignite::sql_connection> conn) {
-    auto args = PyTuple_New(0);
-    auto kwargs = Py_BuildValue("{}");
-    PyObject* py_conn_obj  = PyObject_Call((PyObject*)&py_connection_type, 
args, kwargs);
-    Py_DECREF(args);
-    Py_DECREF(kwargs);
+    py_connection* py_conn_obj  = PyObject_New(py_connection, 
&py_connection_type);
 
     if (!py_conn_obj)
         return nullptr;
 
-    auto typed_conn = reinterpret_cast<py_connection*>(py_conn_obj);
-    typed_conn->m_env = env.release();
-    typed_conn->m_conn = conn.release();
+    py_conn_obj->m_environment = env.release();
+    py_conn_obj->m_connection = conn.release();
 
-    return typed_conn;
+    return py_conn_obj;
 }
diff --git a/modules/platforms/python/cpp_module/py_connection.h 
b/modules/platforms/python/cpp_module/py_connection.h
index c71d1bcb98..246ac3b0a6 100644
--- a/modules/platforms/python/cpp_module/py_connection.h
+++ b/modules/platforms/python/cpp_module/py_connection.h
@@ -33,10 +33,10 @@ struct py_connection {
     PyObject_HEAD
 
     /** Environment. */
-    ignite::sql_environment *m_env;
+    ignite::sql_environment *m_environment;
 
     /** Connection. */
-    ignite::sql_connection *m_conn;
+    ignite::sql_connection *m_connection;
 };
 
 /**
diff --git a/modules/platforms/python/cpp_module/py_cursor.cpp 
b/modules/platforms/python/cpp_module/py_cursor.cpp
new file mode 100644
index 0000000000..d3f76871d6
--- /dev/null
+++ b/modules/platforms/python/cpp_module/py_cursor.cpp
@@ -0,0 +1,313 @@
+/*
+ * 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.
+ */
+
+#include <ignite/odbc/sql_statement.h>
+
+#include <ignite/common/detail/config.h>
+
+#include "module.h"
+#include "py_cursor.h"
+
+#include <Python.h>
+
+int py_cursor_init(py_cursor *self, PyObject *args, PyObject *kwds)
+{
+    UNUSED_VALUE args;
+    UNUSED_VALUE kwds;
+
+    self->m_statement = nullptr;
+
+    return 0;
+}
+
+void py_cursor_dealloc(py_cursor *self)
+{
+    delete self->m_statement;
+    self->m_statement = nullptr;
+    Py_TYPE(self)->tp_free(self);
+}
+
+static PyObject* py_cursor_close(py_cursor* self, PyObject*)
+{
+    if (self->m_statement) {
+        self->m_statement->close();
+        if (!check_errors(*self->m_statement))
+            return nullptr;
+
+        delete self->m_statement;
+        self->m_statement = nullptr;
+    }
+
+    Py_INCREF(Py_None);
+    return Py_None;
+}
+
+static PyObject* py_cursor_execute(py_cursor* self, PyObject* args, PyObject* 
kwargs)
+{
+    if (!self->m_statement) {
+        PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state 
(Already closed?)");
+        return nullptr;
+    }
+
+    static char *kwlist[] = {
+        "query",
+        "params",
+        nullptr
+    };
+
+    const char* query = nullptr;
+    // TODO IGNITE-22741 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);
+    if (!check_errors(*self->m_statement))
+        return nullptr;
+
+    Py_INCREF(Py_None);
+    return Py_None;
+}
+
+static PyObject* py_cursor_rowcount(py_cursor* self, PyObject*)
+{
+    if (!self->m_statement) {
+        PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state 
(Already closed?)");
+        return nullptr;
+    }
+
+    auto query = self->m_statement->get_query();
+
+    if (!query)
+        return PyLong_FromLong(-1);
+
+    return PyLong_FromLong(long(query->affected_rows()));
+}
+
+static PyObject* py_cursor_column_count(py_cursor* self, PyObject*)
+{
+    if (!self->m_statement) {
+        PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state 
(Already closed?)");
+        return nullptr;
+    }
+
+    auto query = self->m_statement->get_query();
+
+    if (!query)
+        return PyLong_FromLong(0);
+
+    return PyLong_FromLong(long(query->get_meta()->size()));
+}
+
+const ignite::column_meta *get_meta_column(py_cursor* self, long idx, PyObject 
*&err_ret) {
+    err_ret = nullptr;
+    auto query = self->m_statement->get_query();
+    if (!query) {
+        Py_INCREF(Py_None);
+        err_ret = Py_None;
+        return nullptr;
+    }
+
+    auto meta = query->get_meta();
+    if (!meta) {
+        Py_INCREF(Py_None);
+        err_ret = Py_None;
+        return nullptr;
+    }
+
+    if (idx < 0 || idx >= long(meta->size())) {
+        PyErr_SetString(PyExc_RuntimeError, "Column metadata index is out of 
bound");
+        return nullptr;
+    }
+
+    return &meta->at(idx);
+}
+
+static PyObject* py_cursor_column_name(py_cursor* self, PyObject* args)
+{
+    if (!self->m_statement) {
+        PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state 
(Already closed?)");
+        return nullptr;
+    }
+
+    long idx{0};
+
+    int parsed = PyArg_ParseTuple(args, "l", &idx);
+    if (!parsed)
+        return nullptr;
+
+    PyObject* err{nullptr};
+    auto column = get_meta_column(self, idx, err);
+    if (!column)
+        return err;
+
+    return PyUnicode_FromStringAndSize(column->get_column_name().data(), 
column->get_column_name().size());
+}
+
+static PyObject* py_cursor_column_type_code(py_cursor* self, PyObject* args)
+{
+    if (!self->m_statement) {
+        PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state 
(Already closed?)");
+        return nullptr;
+    }
+
+    long idx{0};
+
+    int parsed = PyArg_ParseTuple(args, "l", &idx);
+    if (!parsed)
+        return nullptr;
+
+    PyObject* err{nullptr};
+    auto column = get_meta_column(self, idx, err);
+    if (!column)
+        return err;
+
+    return PyLong_FromLong(long(column->get_data_type()));
+}
+
+static PyObject* py_cursor_column_display_size(py_cursor* self, PyObject* args)
+{
+    if (!self->m_statement) {
+        PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state 
(Already closed?)");
+        return nullptr;
+    }
+
+    Py_INCREF(Py_None);
+    return Py_None;
+}
+
+static PyObject* py_cursor_column_internal_size(py_cursor* self, PyObject* 
args)
+{
+    if (!self->m_statement) {
+        PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state 
(Already closed?)");
+        return nullptr;
+    }
+
+    Py_INCREF(Py_None);
+    return Py_None;
+}
+
+static PyObject* py_cursor_column_precision(py_cursor* self, PyObject* args)
+{
+    if (!self->m_statement) {
+        PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state 
(Already closed?)");
+        return nullptr;
+    }
+
+    long idx{0};
+
+    int parsed = PyArg_ParseTuple(args, "l", &idx);
+    if (!parsed)
+        return nullptr;
+
+    PyObject* err{nullptr};
+    auto column = get_meta_column(self, idx, err);
+    if (!column)
+        return err;
+
+    return PyLong_FromLong(long(column->get_precision()));
+}
+
+static PyObject* py_cursor_column_scale(py_cursor* self, PyObject* args)
+{
+    if (!self->m_statement) {
+        PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state 
(Already closed?)");
+        return nullptr;
+    }
+
+    long idx{0};
+
+    int parsed = PyArg_ParseTuple(args, "l", &idx);
+    if (!parsed)
+        return nullptr;
+
+    PyObject* err{nullptr};
+    auto column = get_meta_column(self, idx, err);
+    if (!column)
+        return err;
+
+    return PyLong_FromLong(long(column->get_scale()));
+}
+
+static PyObject* py_cursor_null_ok(py_cursor* self, PyObject* args)
+{
+    if (!self->m_statement) {
+        PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state 
(Already closed?)");
+        return nullptr;
+    }
+
+    long idx{0};
+
+    int parsed = PyArg_ParseTuple(args, "l", &idx);
+    if (!parsed)
+        return nullptr;
+
+    PyObject* err{nullptr};
+    auto column = get_meta_column(self, idx, err);
+    if (!column)
+        return err;
+
+    return PyBool_FromLong(long(column->get_nullability() == 
ignite::nullability::NULLABLE));
+}
+
+static PyTypeObject py_cursor_type = {
+    PyVarObject_HEAD_INIT(nullptr, 0)
+    MODULE_NAME "." PY_CURSOR_CLASS_NAME
+};
+
+static struct PyMethodDef py_cursor_methods[] = {
+    {"close", (PyCFunction)py_cursor_close, METH_NOARGS, nullptr},
+    {"execute", (PyCFunction)py_cursor_execute, METH_VARARGS | METH_KEYWORDS, 
nullptr},
+    {"rowcount", (PyCFunction)py_cursor_rowcount, METH_NOARGS, nullptr},
+    {"column_count", (PyCFunction)py_cursor_column_count, METH_NOARGS, 
nullptr},
+    {"column_name", (PyCFunction)py_cursor_column_name, METH_VARARGS, nullptr},
+    {"column_type_code", (PyCFunction)py_cursor_column_type_code, 
METH_VARARGS, nullptr},
+    {"column_display_size", (PyCFunction)py_cursor_column_display_size, 
METH_VARARGS, nullptr},
+    {"column_internal_size", (PyCFunction)py_cursor_column_internal_size, 
METH_VARARGS, nullptr},
+    {"column_precision", (PyCFunction)py_cursor_column_precision, 
METH_VARARGS, nullptr},
+    {"column_scale", (PyCFunction)py_cursor_column_scale, METH_VARARGS, 
nullptr},
+    {"column_null_ok", (PyCFunction)py_cursor_null_ok, METH_VARARGS, nullptr},
+    {nullptr, nullptr, 0, nullptr}
+};
+
+int prepare_py_cursor_type() {
+    py_cursor_type.tp_new = PyType_GenericNew;
+    py_cursor_type.tp_basicsize=sizeof(py_cursor);
+    py_cursor_type.tp_dealloc=(destructor)py_cursor_dealloc;
+    py_cursor_type.tp_flags=Py_TPFLAGS_DEFAULT;
+    py_cursor_type.tp_methods=py_cursor_methods;
+    py_cursor_type.tp_init=(initproc)py_cursor_init;
+
+    return PyType_Ready(&py_cursor_type);
+}
+
+int register_py_cursor_type(PyObject* mod) {
+    return PyModule_AddObjectRef(mod, PY_CURSOR_CLASS_NAME, (PyObject 
*)&py_cursor_type);
+}
+
+py_cursor *make_py_cursor(std::unique_ptr<ignite::sql_statement> stmt) {
+    py_cursor* py_cursor_obj  = PyObject_New(py_cursor, &py_cursor_type);
+    if (!py_cursor_obj)
+        return nullptr;
+
+    py_cursor_obj->m_statement = stmt.release();
+
+    return py_cursor_obj;
+}
diff --git a/modules/platforms/python/cpp_module/py_connection.h 
b/modules/platforms/python/cpp_module/py_cursor.h
similarity index 56%
copy from modules/platforms/python/cpp_module/py_connection.h
copy to modules/platforms/python/cpp_module/py_cursor.h
index c71d1bcb98..035b21f1ba 100644
--- a/modules/platforms/python/cpp_module/py_connection.h
+++ b/modules/platforms/python/cpp_module/py_cursor.h
@@ -19,52 +19,46 @@
 
 #include <Python.h>
 
-#define PY_CONNECTION_CLASS_NAME "PyConnection"
+#define PY_CURSOR_CLASS_NAME "PyCursor"
 
 namespace ignite {
-class sql_environment;
-class sql_connection;
+class sql_statement;
 }
 
 /**
- * Connection Python object.
+ * Cursor Python object.
  */
-struct py_connection {
+struct py_cursor {
     PyObject_HEAD
 
-    /** Environment. */
-    ignite::sql_environment *m_env;
-
-    /** Connection. */
-    ignite::sql_connection *m_conn;
+    /** Statement. */
+    ignite::sql_statement *m_statement;
 };
 
 /**
  * Connection init function.
  */
-int py_connection_init(py_connection *self, PyObject *args, PyObject *kwds);
+int py_cursor_init(py_cursor *self, PyObject *args, PyObject *kwds);
 
 /**
  * Connection dealloc function.
  */
-void py_connection_dealloc(py_connection *self);
+void py_cursor_dealloc(py_cursor *self);
 
 /**
- * Create a new instance of py_connection python class.
+ * Create a new instance of py_cursor python class.
  *
- * @param env Environment.
- * @param conn Connection.
+ * @param stmt Statement.
  * @return A new class instance.
  */
-py_connection* make_py_connection(std::unique_ptr<ignite::sql_environment> env,
-    std::unique_ptr<ignite::sql_connection> conn);
+py_cursor* make_py_cursor(std::unique_ptr<ignite::sql_statement> stmt);
 
 /**
- * Prepare PyConnection type for registration.
+ * Prepare PyCursor type for registration.
  */
-int prepare_py_connection_type();
+int prepare_py_cursor_type();
 
 /**
- * Register PyConnection type within module.
+ * Register PyCursor type within module.
  */
-int register_py_connection_type(PyObject* mod);
\ No newline at end of file
+int register_py_cursor_type(PyObject* mod);
diff --git a/modules/platforms/python/pyignite3/__init__.py 
b/modules/platforms/python/pyignite3/__init__.py
index e2f01c0f1b..fabe96ac62 100644
--- a/modules/platforms/python/pyignite3/__init__.py
+++ b/modules/platforms/python/pyignite3/__init__.py
@@ -12,8 +12,13 @@
 # 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 datetime
+import decimal
+import uuid
+from typing import Optional, List
 
 from pyignite3 import _pyignite3_extension
+from pyignite3 import native_type_code
 
 __version__ = '3.0.0-beta2'
 
@@ -26,50 +31,205 @@ threadsafety = 1
 # Parameter style is a question mark, e.g. '...WHERE name=?'
 paramstyle = 'qmark'
 
+NIL = None
+BOOLEAN = bool
+INT = int
+FLOAT = float
+STRING = str
+BINARY = memoryview
+NUMBER = decimal.Decimal
+DATE = datetime.date
+TIME = datetime.time
+DATETIME = datetime.datetime
+UUID = uuid.UUID
+
+
+def type_code_from_int(native: int):
+    match native:
+        case native_type_code.NIL:
+            return NIL
+        case native_type_code.BOOLEAN:
+            return BOOLEAN
+        case native_type_code.INT8 | native_type_code.INT16 | 
native_type_code.INT32 | native_type_code.INT64:
+            return INT
+        case native_type_code.FLOAT | native_type_code.DOUBLE:
+            return FLOAT
+        case native_type_code.DECIMAL | native_type_code.NUMBER:
+            return NUMBER
+        case native_type_code.DATE:
+            return DATE
+        case native_type_code.TIME:
+            return TIME
+        case native_type_code.DATETIME | native_type_code.TIMESTAMP:
+            return DATETIME
+        case native_type_code.UUID:
+            return UUID
+        case native_type_code.BITMASK:
+            return INT
+        case native_type_code.STRING:
+            return STRING
+        case native_type_code.BYTE_ARRAY:
+            return BINARY
+        case native_type_code.PERIOD | native_type_code.DURATION:
+            return DATETIME
+
+
+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.display_size = display_size
+        self.internal_size = internal_size
+        self.precision = precision
+        self.scale = scale
+        self.null_ok = null_ok
+
 
 class Cursor:
+    """
+    Cursor class. Represents a single statement and holds the result of its 
execution.
+    """
+    def __init__(self, py_cursor):
+        self._py_cursor = py_cursor
+
+        # TODO: IGNITE-22741 Implement data fetching
+        self.arraysize = 1
+        self._description = None
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.close()
+
+    @property
+    def description(self) -> Optional[List[ColumnDescription]]:
+        """
+        This read-only attribute is a sequence of 7-item sequences.
+        Each of these sequences contains information describing one result 
column:
+        - name
+        - type_code
+        - display_size
+        - internal_size
+        - precision
+        - scale
+        - null_ok
+        The first two items (name and type_code) are mandatory, the other five 
are optional and are set to None if
+        no meaningful values can be provided.
+        This attribute will be None for operations that do not return rows or 
if the cursor has not had an operation
+        invoked via the .execute*() method yet.
+        """
+        if self._py_cursor is None:
+            return None
+        return self._description
+
+    @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).
+        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.
+        """
+        if self._py_cursor is None:
+            return -1
+        return self._py_cursor.rowcount()
+
     def callproc(self, *args):
-        # TODO: IGNITE-22226 Implement cursor support
+        if self._py_cursor is None:
+            raise InterfaceError('Connection is already closed')
+
         raise NotSupportedError('Stored procedures are not supported')
 
     def close(self):
-        # TODO: IGNITE-22226 Implement cursor support
-        raise NotSupportedError('Operation is not supported')
+        """
+        Close active cursor.
+        Completes without errors on successfully closed cursors.
+        """
+        if self._py_cursor is not None:
+            self._py_cursor.close()
+            self._py_cursor = None
 
     def execute(self, *args):
-        # TODO: IGNITE-22226 Implement cursor support
-        raise NotSupportedError('Operation is not supported')
+        """
+        Execute a database operation (query or command).
+
+        Parameters may be provided as sequence or mapping and will be bound to 
variables in the operation.
+        Arguments are specified as a question mark '?' in the request.
+
+        The parameters may also be specified as list of tuples to e.g. insert 
multiple rows in a single operation,
+        but this kind of usage is deprecated: .executemany() should be used 
instead.
+        """
+        if self._py_cursor is None:
+            raise InterfaceError('Connection is already closed')
+
+        self._py_cursor.execute(*args)
+        self._update_description()
+
+    def _update_description(self):
+        """
+        Update column description for the current cursor. To be called after 
query execution.
+        """
+        self._description = []
+        for column_id in range(self._py_cursor.column_count()):
+            self._description.append(ColumnDescription(
+                name=self._py_cursor.column_name(column_id),
+                type_code=self._py_cursor.column_type_code(column_id),
+                display_size=self._py_cursor.column_display_size(column_id),
+                internal_size=self._py_cursor.column_internal_size(column_id),
+                precision=self._py_cursor.column_precision(column_id),
+                scale=self._py_cursor.column_scale(column_id),
+                null_ok=self._py_cursor.column_null_ok(column_id)
+            ))
 
     def executemany(self, *args):
-        # TODO: IGNITE-22226 Implement cursor support
+        if self._py_cursor is None:
+            raise InterfaceError('Connection is already closed')
+
+        # TODO: IGNITE-22742 Implement execution with a batch of parameters
         raise NotSupportedError('Operation is not supported')
 
     def fetchone(self):
-        # TODO: IGNITE-22226 Implement cursor support
+        if self._py_cursor is None:
+            raise InterfaceError('Connection is already closed')
+
+        # TODO: IGNITE-22741 Implement data fetching
         raise NotSupportedError('Operation is not supported')
 
     def fetchmany(self):
-        # TODO: IGNITE-22226 Implement cursor support
+        if self._py_cursor is None:
+            raise InterfaceError('Connection is already closed')
+
+        # TODO: IGNITE-22741 Implement data fetching
         raise NotSupportedError('Operation is not supported')
 
     def fetchall(self):
-        # TODO: IGNITE-22226 Implement cursor support
+        if self._py_cursor is None:
+            raise InterfaceError('Connection is already closed')
+
+        # TODO: IGNITE-22741 Implement data fetching
         raise NotSupportedError('Operation is not supported')
 
     def nextset(self):
-        # TODO: IGNITE-22226 Implement cursor support
-        raise NotSupportedError('Operation is not supported')
+        if self._py_cursor is None:
+            raise InterfaceError('Connection is already closed')
 
-    def arraysize(self) -> int:
-        # TODO: IGNITE-22226 Implement cursor support
+        # TODO: IGNITE-22743 Implement execution of SQL scripts
         raise NotSupportedError('Operation is not supported')
 
     def setinputsizes(self, *args):
-        # TODO: IGNITE-22226 Implement cursor support
+        if self._py_cursor is None:
+            raise InterfaceError('Connection is already closed')
+
+        # TODO: IGNITE-22742 Implement execution with a batch of parameters
         raise NotSupportedError('Operation is not supported')
 
     def setoutputsize(self, *args):
-        # TODO: IGNITE-22226 Implement cursor support
+        if self._py_cursor is None:
+            raise InterfaceError('Connection is already closed')
+
+        # TODO: IGNITE-22741 Implement data fetching
         raise NotSupportedError('Operation is not supported')
 
 
@@ -80,6 +240,12 @@ class Connection:
     def __init__(self):
         self._py_connection = None
 
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.close()
+
     def close(self):
         """
         Close active connection.
@@ -90,16 +256,23 @@ class Connection:
             self._py_connection = None
 
     def commit(self):
-        # TODO: IGNITE-22226 Implement transaction support
+        if self._py_connection is None:
+            raise InterfaceError('Connection is already closed')
+
+        # TODO: IGNITE-22740 Implement transaction support
         raise NotSupportedError('Transactions are not supported')
 
     def rollback(self):
-        # TODO: IGNITE-22226 Implement transaction support
+        if self._py_connection is None:
+            raise InterfaceError('Connection is already closed')
+
+        # TODO: IGNITE-22740 Implement transaction support
         raise NotSupportedError('Transactions are not supported')
 
     def cursor(self) -> Cursor:
-        # TODO: IGNITE-22226 Implement cursor support
-        raise NotSupportedError('Operation is not supported')
+        if self._py_connection is None:
+            raise InterfaceError('Connection is already closed')
+        return Cursor(self._py_connection.cursor())
 
 
 def connect(**kwargs) -> Connection:
diff --git a/modules/platforms/python/tests/test_connect.py 
b/modules/platforms/python/pyignite3/native_type_code.py
similarity index 64%
copy from modules/platforms/python/tests/test_connect.py
copy to modules/platforms/python/pyignite3/native_type_code.py
index 5dbd2d21d1..0799d7600e 100644
--- a/modules/platforms/python/tests/test_connect.py
+++ b/modules/platforms/python/pyignite3/native_type_code.py
@@ -12,22 +12,43 @@
 # 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
+NIL = 0
 
+BOOLEAN = 1
 
[email protected](autouse=True)
-def cluster():
-    if not check_cluster_started():
-        yield from start_cluster_gen()
+INT8 = 2
 
+INT16 = 3
 
-def test_check_connection_success():
-    # TODO: Move cluster addresses in const
-    conn = pyignite3.connect(address=server_addresses_basic[0])
-    assert conn is not None
-    conn.close()
+INT32 = 4
 
+INT64 = 5
 
+FLOAT = 6
+
+DOUBLE = 7
+
+DECIMAL = 8
+
+DATE = 9
+
+TIME = 10
+
+DATETIME = 11
+
+TIMESTAMP = 12
+
+UUID = 13
+
+BITMASK = 14
+
+STRING = 15
+
+BYTE_ARRAY = 16
+
+PERIOD = 17
+
+DURATION = 18
+
+NUMBER = 19
diff --git a/modules/platforms/python/tests/test_connect.py 
b/modules/platforms/python/tests/test_connect.py
index 5dbd2d21d1..44e6fec6ea 100644
--- a/modules/platforms/python/tests/test_connect.py
+++ b/modules/platforms/python/tests/test_connect.py
@@ -15,19 +15,24 @@
 import pytest
 
 import pyignite3
-from tests.util import start_cluster_gen, check_cluster_started, 
server_addresses_basic
+from tests.util import start_cluster_gen, check_cluster_started, 
server_addresses_invalid, server_addresses_basic
 
 
 @pytest.fixture(autouse=True)
 def cluster():
     if not check_cluster_started():
         yield from start_cluster_gen()
+    else:
+        yield None
 
 
-def test_check_connection_success():
-    # TODO: Move cluster addresses in const
+def test_connection_success():
     conn = pyignite3.connect(address=server_addresses_basic[0])
     assert conn is not None
     conn.close()
 
 
+def test_connection_fail():
+    with pytest.raises(RuntimeError) as err:
+        pyignite3.connect(address=server_addresses_invalid[0])
+    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
new file mode 100644
index 0000000000..0efa936a43
--- /dev/null
+++ b/modules/platforms/python/tests/test_execute.py
@@ -0,0 +1,99 @@
+# 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 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
+
+
+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
+
+            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()
+
+
+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 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
+
+                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
+
+            finally:
+                cursor.execute(f'drop table if exists {table_name}');
+
+
+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
+
+            finally:
+                cursor.execute(f'drop table if exists {table_name}');
diff --git a/modules/platforms/python/tests/util.py 
b/modules/platforms/python/tests/util.py
index fa8010d17e..d26b4e5edc 100644
--- a/modules/platforms/python/tests/util.py
+++ b/modules/platforms/python/tests/util.py
@@ -13,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import contextlib
 import os
 
 import psutil
@@ -23,21 +22,14 @@ import time
 
 import pyignite3
 
-server_addresses_basic = ['127.0.0.1:10942', '127.0.0.1:10943']
-server_addresses_ssl_basic = ['127.0.0.1:10944']
-server_addresses_ssl_client_auth = ['127.0.0.1:10945']
+server_host = os.getenv("IGNITE_CLUSTER_HOST", '127.0.0.1')
+server_addresses_invalid = [server_host + ':10000']
+server_addresses_basic = [server_host + ':10942', server_host + ':10943']
+server_addresses_ssl_basic = [server_host + ':10944']
+server_addresses_ssl_client_auth = [server_host + ':10945']
 server_addresses_all = server_addresses_basic + server_addresses_ssl_basic + 
server_addresses_ssl_client_auth
 
 
[email protected]
-def get_or_create_cache(client, settings):
-    cache = client.get_or_create_cache(settings)
-    try:
-        yield cache
-    finally:
-        cache.destroy()
-
-
 def wait_for_condition(condition, interval=0.1, timeout=10, error=None):
     start = time.time()
     res = condition()
@@ -101,7 +93,7 @@ def kill_process_tree(pid):
 def check_server_started(addr: str) -> bool:
     try:
         conn = pyignite3.connect(address=addr, timeout=1)
-    except:
+    except RuntimeError as e:
         return False
 
     conn.close()


Reply via email to