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 4abfb8a74f IGNITE-22742 DB API Driver: Implement execution with a
batch of parameters (#4704)
4abfb8a74f is described below
commit 4abfb8a74f632abaeed246342522a8628f2e889d
Author: Igor Sapego <[email protected]>
AuthorDate: Wed Nov 13 10:36:29 2024 +0400
IGNITE-22742 DB API Driver: Implement execution with a batch of parameters
(#4704)
---
modules/platforms/build.gradle | 14 +-
modules/platforms/python/cpp_module/py_cursor.cpp | 230 +++++++++++++++++++--
modules/platforms/python/pyignite3/__init__.py | 20 +-
modules/platforms/python/pyignite3/_version.txt | 2 +-
.../python/tests/test_dbapi_compliance.py | 3 +-
modules/platforms/python/tests/test_executemany.py | 33 +++
6 files changed, 263 insertions(+), 39 deletions(-)
diff --git a/modules/platforms/build.gradle b/modules/platforms/build.gradle
index 44c93e4dce..24d75e2de6 100644
--- a/modules/platforms/build.gradle
+++ b/modules/platforms/build.gradle
@@ -206,8 +206,8 @@ artifacts {
builtBy cmakeBuildClient
}
- def cmakeProjectVersion =
cmakeProjectVersion(project.projectVersion.toString())
-
cppClient(file("$buildDir/cpp/lib/libignite3-client.so.$cmakeProjectVersion")) {
+ def simpleProjectVersion =
simpleProjectVersion(project.projectVersion.toString())
+
cppClient(file("$buildDir/cpp/lib/libignite3-client.so.$simpleProjectVersion"))
{
builtBy cmakeBuildClient
}
cppClient(file("$buildDir/cpp/lib/libignite3-client.so")) {
@@ -222,7 +222,7 @@ artifacts {
odbc(file("$buildDir/cpp/lib/libignite3-odbc.so")) {
builtBy cmakeBuildOdbc
}
-
odbc(file("$buildDir/cpp/lib/libignite3-odbc.so.$cmakeProjectVersion")) {
+
odbc(file("$buildDir/cpp/lib/libignite3-odbc.so.$simpleProjectVersion")) {
builtBy cmakeBuildOdbc
}
}
@@ -239,13 +239,13 @@ def updateVersion = tasks.register('updateVersion', Task)
{
private void updateCMakeVersion(String version) {
def versionFile = file("$projectDir/cpp/_version.txt")
- versionFile.text = cmakeProjectVersion(version)
+ versionFile.text = simpleProjectVersion(version)
}
/**
- * Strips the pre-release portion of the project version string to satisfy
CMake requirements
+ * Strips the pre-release portion of the project version string
*/
-private String cmakeProjectVersion(String version) {
+private String simpleProjectVersion(String version) {
def dashIndex = version.indexOf('-')
if (dashIndex != -1) {
return version.substring(0, dashIndex)
@@ -263,5 +263,5 @@ private void updateDotnetVersion(String version) {
private void updatePythonVersion(String version) {
def versionFile = file("$projectDir/python/pyignite3/_version.txt")
- versionFile.text = version
+ versionFile.text = simpleProjectVersion(version)
}
diff --git a/modules/platforms/python/cpp_module/py_cursor.cpp
b/modules/platforms/python/cpp_module/py_cursor.cpp
index 80801e34b5..e8af04414b 100644
--- a/modules/platforms/python/cpp_module/py_cursor.cpp
+++ b/modules/platforms/python/cpp_module/py_cursor.cpp
@@ -24,6 +24,50 @@
#include <Python.h>
+/**
+ * Write row of the param set using provided writer.
+ *
+ * @param writer Writer.
+ * @param params_row Parameter Row.
+ */
+void write_row(ignite::protocol::writer &writer, PyObject *params_row,
std::int32_t row_size_expected) {
+ if (!params_row || params_row == Py_None) {
+ throw ignite::ignite_error("Parameter row can not be None");
+ }
+
+ if (!PySequence_Check(params_row)) {
+ throw ignite::ignite_error(std::string("Parameter row does not provide
the sequence protocol: ") +
+ py_object_get_typename(params_row));
+ }
+
+ Py_ssize_t seq_size{PySequence_Size(params_row)};
+ if (seq_size < 0) {
+ throw ignite::ignite_error("Internal error while getting size of the
parameter list sequence");
+ }
+
+ auto row_size = std::int32_t(seq_size);
+ if (row_size != row_size_expected) {
+ throw ignite::ignite_error("Row size is unexpected: " +
std::to_string(row_size) +
+ ", expected row size: " + std::to_string(row_size_expected));
+ }
+
+ 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(params_row, idx),
true);
+ }
+
+ row_builder.layout();
+
+ for (std::int32_t idx = 0; idx < row_size; ++idx) {
+ submit_pyobject(row_builder, PySequence_GetItem(params_row, idx),
false);
+ }
+
+ auto row_data = row_builder.build();
+ writer.write_binary(row_data);
+}
+
/**
* Python parameter set.
*/
@@ -32,6 +76,7 @@ public:
/**
* Constructor.
*
+ * @param size Size of the row.
* @param params Python parameters sequence.
*/
py_parameter_set(Py_ssize_t size, PyObject *params) : m_size(size),
m_params(params) {}
@@ -42,28 +87,94 @@ public:
* @param writer Writer.
*/
virtual void write(ignite::protocol::writer &writer) const override {
- auto row_size = std::int32_t(m_size);
- if (!row_size) {
+ if (!m_size) {
writer.write_nil();
return;
}
- writer.write(row_size);
- ignite::binary_tuple_builder row_builder{row_size * 3};
- row_builder.start();
+ writer.write(m_size);
+ write_row(writer, m_params, m_size);
+ }
- for (std::int32_t idx = 0; idx < row_size; ++idx) {
- submit_pyobject(row_builder, PySequence_GetItem(m_params, idx),
true);
- }
+ /**
+ * 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 {
+ 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 {
+ 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
{
+ return nullptr;
+ }
+
+private:
+ /** Size. */
+ Py_ssize_t m_size{0};
+
+ /** Python sequence of parameters. */
+ PyObject *m_params{nullptr};
+
+ /** Processed params. */
+ SQLULEN m_processed{0};
+};
- row_builder.layout();
+/**
+ * Python parameter list set.
+ */
+class py_parameter_list_set : public ignite::parameter_set {
+public:
+ /**
+ * Constructor.
+ *
+ * @param size Size number of rows to insert.
+ * @param row_size Number of params in a single row.
+ * @param params Python parameter sequence list.
+ */
+ py_parameter_list_set(Py_ssize_t size, Py_ssize_t row_size, PyObject
*params)
+ : m_size(size)
+ , m_row_size(row_size)
+ , m_params(params) {}
- for (std::int32_t idx = 0; idx < row_size; ++idx) {
- submit_pyobject(row_builder, PySequence_GetItem(m_params, idx),
false);
+ /**
+ * Write only first row of the param set using provided writer.
+ *
+ * @param writer Writer.
+ */
+ virtual void write(ignite::protocol::writer &writer) const override {
+ PyObject *row = PySequence_GetItem(m_params, 0);
+ if (!m_row_size) {
+ writer.write_nil();
+ return;
}
- auto row_data = row_builder.build();
- writer.write_binary(row_data);
+ writer.write(m_row_size);
+ write_row(writer, row, m_row_size);
}
/**
@@ -75,8 +186,17 @@ public:
* @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");
+ Py_ssize_t interval_end = std::min(m_size, Py_ssize_t(end));
+ std::int32_t rows_num = std::int32_t(interval_end) -
std::int32_t(begin);
+
+ writer.write(std::int32_t(m_row_size));
+ writer.write(rows_num);
+ writer.write_bool(last);
+
+ for (Py_ssize_t i = Py_ssize_t(begin); i < interval_end; ++i) {
+ PyObject *row = PySequence_GetItem(m_params, i);
+ write_row(writer, row, m_row_size);
+ }
}
/**
@@ -85,8 +205,7 @@ public:
* @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;
+ return std::int32_t(m_size);
}
/**
@@ -102,14 +221,16 @@ public:
* @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. */
+ /** Rows number. */
Py_ssize_t m_size{0};
+ /** Row size. */
+ Py_ssize_t m_row_size{0};
+
/** Python sequence of parameters. */
PyObject *m_params{nullptr};
@@ -207,6 +328,76 @@ static PyObject* py_cursor_execute(py_cursor* self,
PyObject* args, PyObject* kw
Py_RETURN_NONE;
}
+static PyObject* py_cursor_executemany(py_cursor* self, PyObject* args,
PyObject* kwargs)
+{
+ if (!py_cursor_expect_open(self))
+ return nullptr;
+
+ static char *kwlist[] = {
+ "query",
+ "params_list",
+ nullptr
+ };
+
+ const char* query = nullptr;
+ PyObject *params_list = nullptr;
+
+ int parsed = PyArg_ParseTupleAndKeywords(args, kwargs, "s|O", kwlist,
&query, ¶ms_list);
+ if (!parsed)
+ return nullptr;
+
+ Py_ssize_t size{0};
+ Py_ssize_t row_size{0};
+ if (params_list && params_list != Py_None) {
+ if (PySequence_Check(params_list)) {
+ size = PySequence_Size(params_list);
+ if (size < 0) {
+ PyErr_SetString(py_get_module_interface_error_class(),
+ "Internal error while getting size of the parameter list
sequence");
+
+ return nullptr;
+ }
+
+ if (size > 0) {
+ PyObject *row0 = PySequence_GetItem(params_list, 0);
+ if (row0 == nullptr) {
+ PyErr_SetString(py_get_module_interface_error_class(),
+ "Can not get a first element of the parameter
sequence");
+ }
+
+ if (!PySequence_Check(row0)) {
+ auto msg_str = std::string(
+ "A first element of the parameter sequence does not
provide the sequence protocol: ")
+ + py_object_get_typename(params_list);
+
+ PyErr_SetString(py_get_module_interface_error_class(),
msg_str.c_str());
+ }
+
+ row_size = PySequence_Size(row0);
+ if (row_size < 0) {
+ PyErr_SetString(py_get_module_interface_error_class(),
+ "Internal error while getting size of the first
parameter row");
+
+ return nullptr;
+ }
+ }
+ } else {
+ auto msg_str = std::string("The object does not provide the
sequence protocol: ")
+ + py_object_get_typename(params_list);
+
+ PyErr_SetString(py_get_module_interface_error_class(),
msg_str.c_str());
+ return nullptr;
+ }
+ }
+
+ py_parameter_list_set py_params_list(size, row_size, params_list);
+ self->m_statement->execute_sql_query(query, py_params_list);
+ if (!check_errors(*self->m_statement))
+ return nullptr;
+
+ Py_RETURN_NONE;
+}
+
static PyObject* py_cursor_rowcount(py_cursor* self, PyObject*)
{
if (!py_cursor_expect_open(self))
@@ -427,6 +618,7 @@ static struct PyMethodDef py_cursor_methods[] = {
// Core methods
{"close", (PyCFunction)py_cursor_close, METH_NOARGS, nullptr},
{"execute", (PyCFunction)py_cursor_execute, METH_VARARGS | METH_KEYWORDS,
nullptr},
+ {"executemany", (PyCFunction)py_cursor_executemany, METH_VARARGS |
METH_KEYWORDS, nullptr},
{"rowcount", (PyCFunction)py_cursor_rowcount, METH_NOARGS, nullptr},
{"fetchone", (PyCFunction)py_cursor_fetchone, METH_NOARGS, nullptr},
// Column metadata retrieval methods
diff --git a/modules/platforms/python/pyignite3/__init__.py
b/modules/platforms/python/pyignite3/__init__.py
index 5358f61862..4c8000923c 100644
--- a/modules/platforms/python/pyignite3/__init__.py
+++ b/modules/platforms/python/pyignite3/__init__.py
@@ -405,7 +405,7 @@ class Cursor:
# noinspection PyProtectedMember
self._conn._cursor_closed(self._cur_id)
- def execute(self, query: str, params: Optional[Union[List[Any],
Tuple[Any]]] = None):
+ def execute(self, query: str, params: Optional[Sequence[Any]] = None):
"""
Execute a database operation (query or command).
@@ -439,12 +439,13 @@ class Cursor:
null_ok=self._py_cursor.column_null_ok(column_id)
))
- def executemany(self, *_args):
+ def executemany(self, query: str, params_list: List[Sequence[Any]]):
if self._py_cursor is None:
raise InterfaceError('Cursor is already closed')
- # TODO: IGNITE-22742 Implement execution with a batch of parameters
- raise NotSupportedError('Operation is not supported')
+ self._py_cursor.executemany(query, params_list)
+ self._update_description()
+ self._rownumber = 0
def fetchone(self) -> Optional[Sequence[Optional[Any]]]:
"""
@@ -525,13 +526,12 @@ class Cursor:
raise NotSupportedError('Operation is not supported')
def setinputsizes(self, *_args):
- if self._py_cursor is None:
- raise InterfaceError('Cursor is already closed')
-
- # TODO: IGNITE-22742 Implement execution with a batch of parameters
- raise NotSupportedError('Operation is not supported')
+ """
+ This operation does nothing currently.
+ """
+ pass
- def setoutputsize(self, *args):
+ def setoutputsize(self, *_args):
"""
This operation does nothing currently.
"""
diff --git a/modules/platforms/python/pyignite3/_version.txt
b/modules/platforms/python/pyignite3/_version.txt
index 096bf47efe..56fea8a08d 100644
--- a/modules/platforms/python/pyignite3/_version.txt
+++ b/modules/platforms/python/pyignite3/_version.txt
@@ -1 +1 @@
-3.0.0-SNAPSHOT
\ No newline at end of file
+3.0.0
\ No newline at end of file
diff --git a/modules/platforms/python/tests/test_dbapi_compliance.py
b/modules/platforms/python/tests/test_dbapi_compliance.py
index da69e7acf4..62ad84af56 100644
--- a/modules/platforms/python/tests/test_dbapi_compliance.py
+++ b/modules/platforms/python/tests/test_dbapi_compliance.py
@@ -44,7 +44,6 @@ class TestPyignite3(dbapi20.DatabaseAPI20Test):
pass
def test_executemany(self):
- # TODO: IGNITE-22742 Implement execution with a batch of parameters
pass
def test_nextset(self):
@@ -56,7 +55,7 @@ class TestPyignite3(dbapi20.DatabaseAPI20Test):
pass
def test_setinputsizes(self):
- # TODO: IGNITE-22742 Implement execution with a batch of parameters
+ # setoutputsize does not do anything currently.
pass
def test_setoutputsize(self):
diff --git a/modules/platforms/python/tests/test_executemany.py
b/modules/platforms/python/tests/test_executemany.py
new file mode 100644
index 0000000000..4c35f94f48
--- /dev/null
+++ b/modules/platforms/python/tests/test_executemany.py
@@ -0,0 +1,33 @@
+# 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
+
+
[email protected]("batch_size", [1, 2, 10, 300])
+def test_executemany_success(table_name, cursor, drop_table_cleanup,
batch_size):
+ test_data = [(i, f'data_{i}') for i in range(batch_size)]
+
+ cursor.execute(f'create table {table_name}(id int primary key, data
varchar)')
+ cursor.executemany(f"insert into {table_name} values(?, ?)", test_data)
+ cursor.execute(f"select id, data from {table_name} order by id")
+
+ for i in range(batch_size):
+ row = cursor.fetchone()
+ row_expected = test_data[i]
+ assert len(row) == len(row_expected)
+ assert row == row_expected
+
+ end = cursor.fetchone()
+ assert end is None