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, &params_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

Reply via email to