Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- adopt official Mapepire protocol types using dataclasses (#96)
- Native async support for PEP 249 interface (replace to_thread wrapper) #95
- Improve public API surface and top-level exports #94
- Fix PEP 249 compliance: cursor.description and result types #91

## [v0.2.0](https://github.com/Mapepire-IBMi/mapepire-python/releases/tag/v0.2.0) - 2024-11-26
- replace `websocket-client` with `websockets`
Expand Down
33 changes: 21 additions & 12 deletions mapepire_python/asyncio/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ def description(self) -> Optional[Sequence[ColumnDescription]]:
return None
return [
(
col.name,
_DB_TYPE_MAP.get(col.type.upper(), str),
col.display_size,
None,
col.precision,
col.scale,
col.nullable,
col.name,
_DB_TYPE_MAP.get(col.type.upper() if col.type else "", str),
col.display_size,
None,
col.precision,
col.scale,
col.nullable,
)
for col in self._metadata.columns
]
Expand Down Expand Up @@ -102,23 +102,32 @@ async def executemany(
await self.execute(operation, params)
return self

def _row_to_tuple(self, row) -> tuple:
if isinstance(row, dict):
if self._metadata and self._metadata.columns:
return tuple(row.get(col.name, None) for col in self._metadata.columns)
return tuple(row.values())
if isinstance(row, (list, tuple)):
return tuple(row)
return row

async def fetchone(self) -> Optional[ResultRow]:
if self._buffer:
return self._buffer.pop(0)
return self._row_to_tuple(self._buffer.pop(0))
if self._is_done or self._query is None:
return None
result = await self._query.fetch_more(rows_to_fetch=1)
self._is_done = result.is_done
self._buffer.extend(result.data or [])
return self._buffer.pop(0) if self._buffer else None
return self._row_to_tuple(self._buffer.pop(0)) if self._buffer else None

async def fetchmany(self, size: Optional[int] = None) -> ResultSet:
if size is None:
size = self.arraysize
rows = []
while len(rows) < size:
if self._buffer:
rows.append(self._buffer.pop(0))
rows.append(self._row_to_tuple(self._buffer.pop(0)))
elif self._is_done or self._query is None:
break
else:
Expand All @@ -128,12 +137,12 @@ async def fetchmany(self, size: Optional[int] = None) -> ResultSet:
return rows

async def fetchall(self) -> ResultSet:
rows = list(self._buffer)
rows = [self._row_to_tuple(r) for r in self._buffer]
self._buffer.clear()
while not self._is_done and self._query is not None:
result = await self._query.fetch_more(rows_to_fetch=100)
self._is_done = result.is_done
rows.extend(result.data or [])
rows.extend(self._row_to_tuple(r) for r in (result.data or []))
return rows

async def close(self) -> None:
Expand Down
50 changes: 46 additions & 4 deletions mapepire_python/core/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@
__all__ = ["Cursor"]

logger = logging.getLogger(__name__)

_DB_TYPE_MAP = {
"VARCHAR": str, "CHAR": str, "CLOB": str, "NVARCHAR": str, "NCHAR": str,
"NCLOB": str, "GRAPHIC": str, "VARGRAPHIC": str, "DBCLOB": str, "XML": str,
"INTEGER": int, "INT": int, "SMALLINT": int, "BIGINT": int,
"DECIMAL": float, "NUMERIC": float, "FLOAT": float, "DOUBLE": float, "REAL": float,
"DECFLOAT": float,
"BOOLEAN": bool,
"DATE": str, "TIME": str, "TIMESTAMP": str,
"BINARY": bytes, "VARBINARY": bytes, "BLOB": bytes,
}


class Cursor(pep249.CursorConnectionMixin, pep249.IterableCursorMixin, pep249.TransactionalCursor):
max_rows = 2147483647

Expand All @@ -39,6 +52,7 @@ def __init__(self, connection: "Connection", job: SQLJob) -> None:
self.query: Optional[Query] = None
self.query_q: deque[Query] = deque(maxlen=20)
self._result_set: Optional[QueryResultSet] = None
self._metadata = None
self.__closed = False
self.__has_results = False

Expand Down Expand Up @@ -104,10 +118,15 @@ def execute(

prepare_result = query.prepare_sql_execute()

qs = QueryResultSet(prepare_result)
self._metadata = qs.metadata if qs.metadata.columns else None

if prepare_result.has_results:
self.query = query
self.__set_has_results(True)
self.query_q.append(query)
else:
self.__set_has_results(False)

if prepare_result.update_count is not None:
self.rowcount = prepare_result.update_count
Expand All @@ -134,7 +153,29 @@ def callproc(self, procname: ProcName, parameters: Optional[ProcArgs] = None) ->
def description(
self,
) -> Optional[Sequence[ColumnDescription]]:
pass
if not self._metadata or not self._metadata.columns:
return None
return [
(
col.name,
_DB_TYPE_MAP.get(col.type.upper() if col.type else "", str),
col.display_size,
None,
col.precision,
col.scale,
col.nullable,
)
for col in self._metadata.columns
]

def _row_to_tuple(self, row: Any) -> tuple:
if isinstance(row, dict):
if self._metadata and self._metadata.columns:
return tuple(row.get(col.name, None) for col in self._metadata.columns)
return tuple(row.values())
if isinstance(row, (list, tuple)):
return tuple(row)
return row

@raise_if_closed
@convert_runtime_errors
Expand All @@ -144,7 +185,8 @@ def fetchone(self) -> Optional[ResultRow]:
res = self.query.fetch_more(rows_to_fetch=1)
if res:
self._result_set = QueryResultSet(res)
return self._result_set.data[0] if self._result_set.data else None
if self._result_set.data:
return self._row_to_tuple(self._result_set.data[0])
return None

@raise_if_closed
Expand All @@ -156,7 +198,7 @@ def fetchall(self) -> ResultSet:
res = self.query.fetch_more(rows_to_fetch=self.max_rows)
if res:
self._result_set = QueryResultSet(res)
return self._result_set.data
return [self._row_to_tuple(row) for row in self._result_set.data]
return []

@raise_if_closed
Expand All @@ -169,7 +211,7 @@ def fetchmany(self, size: Optional[int] = None) -> ResultSet:
res = self.query.fetch_more(rows_to_fetch=size)
if res:
self._result_set = QueryResultSet(res)
return self._result_set.data
return [self._row_to_tuple(row) for row in self._result_set.data]
return []

def executescript(self, script: SQLQuery) -> "Cursor":
Expand Down
19 changes: 12 additions & 7 deletions mapepire_python/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import dataclasses
from functools import wraps
from typing import Callable, List, Optional
from typing import Any, Callable, Dict, List, Optional, cast

from .exceptions import CONNECTION_CLOSED, ProgrammingError, ReturnType

Expand Down Expand Up @@ -49,11 +49,15 @@ def wrapped(*args, **kwargs):


class ColumnMetaData:
def __init__(self, name: str, type: str, display_size: int, label: str):
def __init__(self, name: str, type: str, display_size: int, label: str,
precision=None, scale=None, nullable=None, **kwargs):
self.name = name
self.type = type
self.display_size = display_size
self.label = label
self.precision = precision
self.scale = scale
self.nullable = nullable


class MetaData:
Expand All @@ -64,17 +68,18 @@ def __init__(self, column_count: int, job: str, columns: List[ColumnMetaData]):


class QueryResultSet:
def __init__(self, result):
def __init__(self, result) -> None:
if dataclasses.is_dataclass(result) and not isinstance(result, type):
result = dataclasses.asdict(result)
result = cast(Dict[str, Any], result)
self.id = result.get("id", None)
self.has_results = result.get("has_results", None)
self.update_count = result.get("update_count", None)
metadata = result.get("metadata", {})
metadata = cast(Dict[str, Any], result.get("metadata") or {})
self.metadata = MetaData(
column_count=metadata.get("column_count", None),
job=metadata.get("job", None),
columns=[ColumnMetaData(**col) for col in metadata.get("columns", [])],
column_count=metadata.get("column_count", 0),
job=metadata.get("job", ""),
columns=[ColumnMetaData(**col) for col in metadata.get("columns", [])], # type: ignore[arg-type]
)
self.data = result.get("data", [])
self.is_done = result.get("is_done", None)
Expand Down
4 changes: 1 addition & 3 deletions tests/async_pool_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@

import pytest

from mapepire_python.client.sql_job import SQLJob
from mapepire_python.data_types import QueryOptions
from mapepire_python.pool.pool_job import PoolJob
from mapepire_python import PoolJob, QueryOptions, SQLJob
from mapepire_python.query_manager import QueryManager

from .test_setup import *
Expand Down
3 changes: 1 addition & 2 deletions tests/cl_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os

from mapepire_python.client.sql_job import SQLJob
from mapepire_python.data_types import QueryOptions
from mapepire_python import QueryOptions, SQLJob

from .test_setup import *

Expand Down
Loading
Loading