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, ¶ms);
+
+ 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()