Skip to content

Commit 2ead886

Browse files
authored
Merge pull request #54 from qaspen-python/feature/add_row_factory
Added row_factory method for query result
2 parents c86c387 + 55fc1cc commit 2ead886

File tree

3 files changed

+144
-5
lines changed

3 files changed

+144
-5
lines changed

python/psqlpy/_internal/__init__.pyi

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ from typing_extensions import Self
88
_CustomClass = TypeVar(
99
"_CustomClass",
1010
)
11+
_RowFactoryRV = TypeVar(
12+
"_RowFactoryRV",
13+
)
1114

1215
class QueryResult:
1316
"""Result."""
@@ -16,7 +19,11 @@ class QueryResult:
1619
self: Self,
1720
custom_decoders: dict[str, Callable[[bytes], Any]] | None = None,
1821
) -> list[dict[Any, Any]]:
19-
"""Return result from database as a list of dicts."""
22+
"""Return result from database as a list of dicts.
23+
24+
`custom_decoders` must be used when you use
25+
PostgreSQL Type which isn't supported, read more in our docs.
26+
"""
2027
def as_class(
2128
self: Self,
2229
as_class: Callable[..., _CustomClass],
@@ -52,12 +59,39 @@ class QueryResult:
5259
)
5360
```
5461
"""
62+
def row_factory(
63+
self,
64+
row_factory: Callable[[dict[str, Any]], _RowFactoryRV],
65+
custom_decoders: dict[str, Callable[[bytes], Any]] | None = None,
66+
) -> list[_RowFactoryRV]:
67+
"""Use custom function to convert results from database.
68+
69+
`custom_decoders` must be used when you use
70+
PostgreSQL Type isn't supported, read more in the docs.
71+
72+
Argument order: firstly we apply `custom_decoders` (if specified),
73+
then we apply `row_factory`.
74+
75+
### Parameters:
76+
- `row_factory`: function which takes `dict[str, Any]` as an argument.
77+
- `custom_decoders`: functions for custom decoding.
78+
79+
### Returns:
80+
List of type that return passed `row_factory`.
81+
"""
5582

5683
class SingleQueryResult:
5784
"""Single result."""
5885

59-
def result(self: Self) -> dict[Any, Any]:
60-
"""Return result from database as a dict."""
86+
def result(
87+
self: Self,
88+
custom_decoders: dict[str, Callable[[bytes], Any]] | None = None,
89+
) -> dict[Any, Any]:
90+
"""Return result from database as a dict.
91+
92+
`custom_decoders` must be used when you use
93+
PostgreSQL Type which isn't supported, read more in our docs.
94+
"""
6195
def as_class(
6296
self: Self,
6397
as_class: Callable[..., _CustomClass],
@@ -96,6 +130,26 @@ class SingleQueryResult:
96130
)
97131
```
98132
"""
133+
def row_factory(
134+
self,
135+
row_factory: Callable[[dict[str, Any]], _RowFactoryRV],
136+
custom_decoders: dict[str, Callable[[bytes], Any]] | None = None,
137+
) -> _RowFactoryRV:
138+
"""Use custom function to convert results from database.
139+
140+
`custom_decoders` must be used when you use
141+
PostgreSQL Type isn't supported, read more in our docs.
142+
143+
Argument order: firstly we apply `custom_decoders` (if specified),
144+
then we apply `row_factory`.
145+
146+
### Parameters:
147+
- `row_factory`: function which takes `list[dict[str, Any]]` as an argument.
148+
- `custom_decoders`: functions for custom decoding.
149+
150+
### Returns:
151+
Type that return passed function.
152+
"""
99153

100154
class IsolationLevel(Enum):
101155
"""Class for Isolation Level for transactions."""

python/tests/test_value_converter.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,3 +571,44 @@ def point_encoder(point_bytes: bytes) -> str:
571571
)
572572

573573
assert result[0]["geo_point"] == "Just An Example"
574+
575+
576+
async def test_row_factory_query_result(
577+
psql_pool: ConnectionPool,
578+
table_name: str,
579+
number_database_records: int,
580+
) -> None:
581+
select_result = await psql_pool.execute(
582+
f"SELECT * FROM {table_name}",
583+
)
584+
585+
def row_factory(db_result: Dict[str, Any]) -> List[str]:
586+
return list(db_result.keys())
587+
588+
as_row_factory = select_result.row_factory(
589+
row_factory=row_factory,
590+
)
591+
assert len(as_row_factory) == number_database_records
592+
593+
assert isinstance(as_row_factory[0], list)
594+
595+
596+
async def test_row_factory_single_query_result(
597+
psql_pool: ConnectionPool,
598+
table_name: str,
599+
) -> None:
600+
connection = await psql_pool.connection()
601+
select_result = await connection.fetch_row(
602+
f"SELECT * FROM {table_name} LIMIT 1",
603+
)
604+
605+
def row_factory(db_result: Dict[str, Any]) -> List[str]:
606+
return list(db_result.keys())
607+
608+
as_row_factory = select_result.row_factory(
609+
row_factory=row_factory,
610+
)
611+
expected_number_of_elements_in_result = 2
612+
assert len(as_row_factory) == expected_number_of_elements_in_result
613+
614+
assert isinstance(as_row_factory, list)

src/query_result.rs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,28 @@ impl PSQLDriverPyQueryResult {
8989

9090
Ok(res.to_object(py))
9191
}
92+
93+
/// Convert result from database with function passed from Python.
94+
///
95+
/// # Errors
96+
///
97+
/// May return Err Result if can not convert
98+
/// postgres type with custom function.
99+
#[allow(clippy::needless_pass_by_value)]
100+
pub fn row_factory<'a>(
101+
&'a self,
102+
py: Python<'a>,
103+
row_factory: Py<PyAny>,
104+
custom_decoders: Option<Py<PyDict>>,
105+
) -> RustPSQLDriverPyResult<Py<PyAny>> {
106+
let mut res: Vec<Py<PyAny>> = vec![];
107+
for row in &self.inner {
108+
let pydict: pyo3::Bound<'_, PyDict> = row_to_dict(py, row, &custom_decoders)?;
109+
let row_factory_class = row_factory.call_bound(py, (pydict,), None)?;
110+
res.push(row_factory_class);
111+
}
112+
Ok(res.to_object(py))
113+
}
92114
}
93115

94116
#[pyclass(name = "SingleQueryResult")]
@@ -121,8 +143,13 @@ impl PSQLDriverSinglePyQueryResult {
121143
/// May return Err Result if can not convert
122144
/// postgres type to python, can not set new key-value pair
123145
/// in python dict or there are no result.
124-
pub fn result(&self, py: Python<'_>) -> RustPSQLDriverPyResult<Py<PyAny>> {
125-
Ok(row_to_dict(py, &self.inner, &None)?.to_object(py))
146+
#[allow(clippy::needless_pass_by_value)]
147+
pub fn result(
148+
&self,
149+
py: Python<'_>,
150+
custom_decoders: Option<Py<PyDict>>,
151+
) -> RustPSQLDriverPyResult<Py<PyAny>> {
152+
Ok(row_to_dict(py, &self.inner, &custom_decoders)?.to_object(py))
126153
}
127154

128155
/// Convert result from database to any class passed from Python.
@@ -141,4 +168,21 @@ impl PSQLDriverSinglePyQueryResult {
141168
let pydict: pyo3::Bound<'_, PyDict> = row_to_dict(py, &self.inner, &None)?;
142169
Ok(as_class.call_bound(py, (), Some(&pydict))?)
143170
}
171+
172+
/// Convert result from database with function passed from Python.
173+
///
174+
/// # Errors
175+
///
176+
/// May return Err Result if can not convert
177+
/// postgres type with custom function
178+
#[allow(clippy::needless_pass_by_value)]
179+
pub fn row_factory<'a>(
180+
&'a self,
181+
py: Python<'a>,
182+
row_factory: Py<PyAny>,
183+
custom_decoders: Option<Py<PyDict>>,
184+
) -> RustPSQLDriverPyResult<Py<PyAny>> {
185+
let pydict = row_to_dict(py, &self.inner, &custom_decoders)?.to_object(py);
186+
Ok(row_factory.call_bound(py, (pydict,), None)?)
187+
}
144188
}

0 commit comments

Comments
 (0)