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 bb51f3cc7c IGNITE-22741 DB API Driver 3: Implement Data Fetching
(#4321)
bb51f3cc7c is described below
commit bb51f3cc7c3df8cc373e27d3d25c55a61a76b630
Author: Igor Sapego <[email protected]>
AuthorDate: Tue Sep 3 12:51:01 2024 +0400
IGNITE-22741 DB API Driver 3: Implement Data Fetching (#4321)
---
.../platforms/cpp/ignite/odbc/query/data_query.cpp | 11 +-
.../platforms/cpp/ignite/odbc/query/data_query.h | 14 ++
modules/platforms/python/cpp_module/py_cursor.cpp | 137 ++++++++++++++-
modules/platforms/python/pyignite3/__init__.py | 73 ++++++--
modules/platforms/python/tests/test_execute.py | 6 +-
.../platforms/python/tests/test_fetch_constants.py | 188 ++++++++++++++++++++
modules/platforms/python/tests/test_fetch_table.py | 193 +++++++++++++++++++++
7 files changed, 600 insertions(+), 22 deletions(-)
diff --git a/modules/platforms/cpp/ignite/odbc/query/data_query.cpp
b/modules/platforms/cpp/ignite/odbc/query/data_query.cpp
index 6f80cead4d..64d3fda363 100644
--- a/modules/platforms/cpp/ignite/odbc/query/data_query.cpp
+++ b/modules/platforms/cpp/ignite/odbc/query/data_query.cpp
@@ -138,7 +138,7 @@ const sql_parameter *data_query::get_sql_param(std::int16_t
idx) {
return nullptr;
}
-sql_result data_query::fetch_next_row(column_binding_map &column_bindings) {
+sql_result data_query::fetch_next_row() {
if (!m_executed) {
m_diag.add_status_record(sql_state::SHY010_SEQUENCE_ERROR, "Query was
not executed.");
@@ -167,6 +167,15 @@ sql_result data_query::fetch_next_row(column_binding_map
&column_bindings) {
if (!m_cursor->has_data())
return sql_result::AI_NO_DATA;
+ return sql_result::AI_SUCCESS;
+}
+
+sql_result data_query::fetch_next_row(column_binding_map &column_bindings) {
+ auto res = fetch_next_row();
+ if (res != ignite::sql_result::AI_SUCCESS && res !=
ignite::sql_result::AI_SUCCESS_WITH_INFO) {
+ return res;
+ }
+
auto row = m_cursor->get_row();
assert(!row.empty());
diff --git a/modules/platforms/cpp/ignite/odbc/query/data_query.h
b/modules/platforms/cpp/ignite/odbc/query/data_query.h
index 396b14bca7..fb12b7b463 100644
--- a/modules/platforms/cpp/ignite/odbc/query/data_query.h
+++ b/modules/platforms/cpp/ignite/odbc/query/data_query.h
@@ -77,6 +77,20 @@ public:
*/
const column_meta_vector *get_meta() override;
+ /**
+ * Fetch next result row.
+ *
+ * @return Operation result.
+ */
+ sql_result fetch_next_row();
+
+ /**
+ * Get current row.
+ *
+ * @return Row.
+ */
+ [[nodiscard]] const std::vector<primitive> &get_current_row() const {
return m_cursor->get_row(); }
+
/**
* Fetch next result row to application buffers.
*
diff --git a/modules/platforms/python/cpp_module/py_cursor.cpp
b/modules/platforms/python/cpp_module/py_cursor.cpp
index 39a4961eb4..ca9fbca610 100644
--- a/modules/platforms/python/cpp_module/py_cursor.cpp
+++ b/modules/platforms/python/cpp_module/py_cursor.cpp
@@ -16,6 +16,7 @@
*/
#include <ignite/odbc/sql_statement.h>
+#include <ignite/odbc/query/data_query.h>
#include <ignite/common/detail/config.h>
@@ -70,7 +71,7 @@ static PyObject* py_cursor_execute(py_cursor* self, PyObject*
args, PyObject* kw
};
const char* query = nullptr;
- // TODO IGNITE-22741 Support parameters
+ // TODO IGNITE-23126 Support parameters
PyObject *params = nullptr;
int parsed = PyArg_ParseTupleAndKeywords(args, kwargs, "s|O", kwlist,
&query, ¶ms);
@@ -101,6 +102,137 @@ 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) {
+ PyErr_SetString(PyExc_RuntimeError, "Cursor is in invalid state
(Already closed?)");
+ return nullptr;
+ }
+
+ auto query = self->m_statement->get_query();
+ if (!query) {
+ PyErr_SetString(PyExc_RuntimeError, "Query was not executed");
+ return nullptr;
+ }
+
+ if (query->get_type() != ignite::query_type::DATA) {
+ auto err_msg = "Unexpected query type: " +
std::to_string(int(query->get_type()));
+ PyErr_SetString(PyExc_RuntimeError, err_msg.c_str());
+ return nullptr;
+ }
+
+ if (!query->is_data_available()) {
+ Py_INCREF(Py_None);
+ return Py_None;
+ }
+
+ auto& query0 = static_cast<ignite::data_query&>(*query);
+ auto res = query0.fetch_next_row();
+ if (res == ignite::sql_result::AI_NO_DATA) {
+ Py_INCREF(Py_None);
+ return Py_None;
+ }
+
+ if (!check_errors(*self->m_statement)) {
+ return nullptr;
+ }
+
+ auto row = query0.get_current_row();
+ auto res_list = PyTuple_New(row.size());
+ if (!res_list) {
+ PyErr_SetString(PyExc_RuntimeError, "Can not allocate a new list for
the result set");
+ return nullptr;
+ }
+
+ for (std::size_t i = 0; i < row.size(); ++i) {
+ auto py_column = primitive_to_pyobject(row[i]);
+ if (!py_column) {
+ Py_DECREF(res_list);
+ return nullptr;
+ }
+ PyTuple_SetItem(res_list, i, py_column);
+ }
+
+ return res_list;
+}
+
static PyObject* py_cursor_column_count(py_cursor* self, PyObject*)
{
if (!self->m_statement) {
@@ -273,9 +405,12 @@ static PyTypeObject py_cursor_type = {
};
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},
{"rowcount", (PyCFunction)py_cursor_rowcount, METH_NOARGS, nullptr},
+ {"fetchone", (PyCFunction)py_cursor_fetchone, METH_NOARGS, nullptr},
+ // Column metadata retrieval methods
{"column_count", (PyCFunction)py_cursor_column_count, METH_NOARGS,
nullptr},
{"column_name", (PyCFunction)py_cursor_column_name, METH_VARARGS, nullptr},
{"column_type_code", (PyCFunction)py_cursor_column_type_code,
METH_VARARGS, nullptr},
diff --git a/modules/platforms/python/pyignite3/__init__.py
b/modules/platforms/python/pyignite3/__init__.py
index e4a77113d8..baf9cf29fb 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
+from typing import Optional, List, Any, Sequence
from pyignite3 import _pyignite3_extension
from pyignite3 import native_type_code
@@ -36,7 +36,7 @@ BOOLEAN = bool
INT = int
FLOAT = float
STRING = str
-BINARY = memoryview
+BINARY = bytes
NUMBER = decimal.Decimal
DATE = datetime.date
TIME = datetime.time
@@ -91,10 +91,10 @@ class Cursor:
"""
Cursor class. Represents a single statement and holds the result of its
execution.
"""
+
def __init__(self, py_cursor):
self._py_cursor = py_cursor
- # TODO: IGNITE-22741 Implement data fetching
self.arraysize = 1
self._description = None
@@ -170,6 +170,7 @@ class Cursor:
def _update_description(self):
"""
+ Internal method.
Update column description for the current cursor. To be called after
query execution.
"""
self._description = []
@@ -191,26 +192,64 @@ class Cursor:
# TODO: IGNITE-22742 Implement execution with a batch of parameters
raise NotSupportedError('Operation is not supported')
- def fetchone(self):
+ def fetchone(self) -> Optional[Sequence[Optional[Any]]]:
+ """
+ Fetch the next row of a query result set, returning a single sequence,
or None when no more data is available.
+ An Error (or subclass) exception is raised if the previous call to
.execute*() did not produce any result set
+ or no call was issued yet.
+ """
if self._py_cursor is None:
raise InterfaceError('Connection is already closed')
- # TODO: IGNITE-22741 Implement data fetching
- raise NotSupportedError('Operation is not supported')
+ return self._py_cursor.fetchone()
+
+ def fetchmany(self, size: Optional[int] = None) ->
Optional[Sequence[Sequence[Optional[Any]]]]:
+ """
+ Fetch the next set of rows of a query result, returning a sequence of
sequences. An empty sequence is returned
+ when no more rows are available.
- def fetchmany(self):
+ The number of rows to fetch per call is specified by the parameter. If
it is not given, the cursor’s arraysize
+ determines the number of rows to be fetched. The method tries to fetch
as many rows as indicated by the size
+ parameter. If this is not possible due to the specified number of rows
not being available, fewer rows will be
+ returned.
+
+ An Error (or subclass) exception is raised if the previous call to
.execute*() did not produce any result set
+ or no call was issued yet.
+ """
if self._py_cursor is None:
raise InterfaceError('Connection is already closed')
- # TODO: IGNITE-22741 Implement data fetching
- raise NotSupportedError('Operation is not supported')
+ if size is None:
+ size = self.arraysize
+
+ if size <= 0:
+ raise InterfaceError(f'Size parameter should be positive
[size={size}]')
+
+ res = []
+ for i in range(size):
+ row = self.fetchone()
+ if row is None:
+ break
+ res.append(row)
+
+ return None if not res else res
- def fetchall(self):
+ def fetchall(self) -> Optional[Sequence[Sequence[Optional[Any]]]]:
+ """
+ Fetch all remaining rows of a query result, returning them as a
sequence of sequences.
+ An Error (or subclass) exception is raised if the previous call to
.execute*() did not produce any result set
+ or no call was issued yet.
+ """
if self._py_cursor is None:
raise InterfaceError('Connection is already closed')
- # TODO: IGNITE-22741 Implement data fetching
- raise NotSupportedError('Operation is not supported')
+ res = []
+ row = self.fetchone()
+ while row is not None:
+ res.append(row)
+ row = self.fetchone()
+
+ return None if not res else res
def nextset(self):
if self._py_cursor is None:
@@ -227,17 +266,17 @@ class Cursor:
raise NotSupportedError('Operation is not supported')
def setoutputsize(self, *args):
- if self._py_cursor is None:
- raise InterfaceError('Connection is already closed')
-
- # TODO: IGNITE-22741 Implement data fetching
- raise NotSupportedError('Operation is not supported')
+ """
+ This operation does nothing currently.
+ """
+ pass
class Connection:
"""
Connection class. Represents a single connection to the Ignite cluster.
"""
+
def __init__(self):
self._py_connection = None
diff --git a/modules/platforms/python/tests/test_execute.py
b/modules/platforms/python/tests/test_execute.py
index 0efa936a43..510f7505d5 100644
--- a/modules/platforms/python/tests/test_execute.py
+++ b/modules/platforms/python/tests/test_execute.py
@@ -15,7 +15,7 @@
import pytest
import pyignite3
-from tests.util import start_cluster_gen, check_cluster_started,
server_addresses_invalid, server_addresses_basic
+from tests.util import start_cluster_gen, check_cluster_started,
server_addresses_basic
@pytest.fixture(autouse=True)
@@ -79,7 +79,7 @@ def test_execute_sql_table_success():
assert cursor.description[2].precision == 3
finally:
- cursor.execute(f'drop table if exists {table_name}');
+ cursor.execute(f'drop table if exists {table_name}')
def test_execute_update_rowcount():
@@ -96,4 +96,4 @@ def test_execute_update_rowcount():
assert cursor.rowcount == 6
finally:
- cursor.execute(f'drop table if exists {table_name}');
+ cursor.execute(f'drop table if exists {table_name}')
diff --git a/modules/platforms/python/tests/test_fetch_constants.py
b/modules/platforms/python/tests/test_fetch_constants.py
new file mode 100644
index 0000000000..3fd7929e13
--- /dev/null
+++ b/modules/platforms/python/tests/test_fetch_constants.py
@@ -0,0 +1,188 @@
+# 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 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()
+ 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''
+
+
+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_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_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'
+
+ nothing = cursor.fetchone()
+ assert nothing is None
diff --git a/modules/platforms/python/tests/test_fetch_table.py
b/modules/platforms/python/tests/test_fetch_table.py
new file mode 100644
index 0000000000..dd9b3d16ab
--- /dev/null
+++ b/modules/platforms/python/tests/test_fetch_table.py
@@ -0,0 +1,193 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import pytest
+
+import pyignite3
+from tests.util import start_cluster_gen, check_cluster_started,
server_addresses_basic
+
+
[email protected](autouse=True)
+def cluster():
+ if not check_cluster_started():
+ yield from start_cluster_gen()
+ else:
+ yield None
+
+
+def create_and_populate_test_table(cursor, rows_num, table_name):
+ cursor.execute(f'drop table if exists {table_name}')
+ cursor.execute(f'create table {table_name}(id int primary key, data
varchar, fl double)')
+ for i in range(rows_num):
+ cursor.execute(f"insert into {table_name} values ({i}, 'Value-{i *
2}', {i / 2.0})")
+
+
+def check_row(i, row):
+ assert len(row) == 3
+ assert row[0] == i
+ assert row[1] == f'Value-{i * 2}'
+ 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)
+
+ cursor.execute(f"select id, data, fl from {table_name} order
by id")
+
+ 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_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])
+
+ end = cursor.fetchone()
+ assert end is None
+
+ finally:
+ cursor.execute(f'drop table if exists {table_name}')