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