Skip to content
This repository was archived by the owner on Aug 19, 2025. It is now read-only.

Commit 0fe0adb

Browse files
laukhincollerek
andauthored
Add _mapping property to the result set interface. (#447)
Co-authored-by: collerek <collerek@gmail.com>
1 parent e28a8d9 commit 0fe0adb

File tree

9 files changed

+100
-24
lines changed

9 files changed

+100
-24
lines changed

databases/backends/aiopg.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414
from sqlalchemy.sql.ddl import DDLElement
1515

1616
from databases.core import DatabaseURL
17-
from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend
17+
from databases.interfaces import (
18+
ConnectionBackend,
19+
DatabaseBackend,
20+
Record,
21+
TransactionBackend,
22+
)
1823

1924
logger = logging.getLogger("databases")
2025

@@ -112,7 +117,7 @@ async def release(self) -> None:
112117
await self._database._pool.release(self._connection)
113118
self._connection = None
114119

115-
async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]:
120+
async def fetch_all(self, query: ClauseElement) -> typing.List[Record]:
116121
assert self._connection is not None, "Connection is not acquired"
117122
query_str, args, context = self._compile(query)
118123
cursor = await self._connection.cursor()
@@ -133,7 +138,7 @@ async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]:
133138
finally:
134139
cursor.close()
135140

136-
async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]:
141+
async def fetch_one(self, query: ClauseElement) -> typing.Optional[Record]:
137142
assert self._connection is not None, "Connection is not acquired"
138143
query_str, args, context = self._compile(query)
139144
cursor = await self._connection.cursor()

databases/backends/asyncmy.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
from sqlalchemy.sql.ddl import DDLElement
1313

1414
from databases.core import LOG_EXTRA, DatabaseURL
15-
from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend
15+
from databases.interfaces import (
16+
ConnectionBackend,
17+
DatabaseBackend,
18+
Record,
19+
TransactionBackend,
20+
)
1621

1722
logger = logging.getLogger("databases")
1823

@@ -100,7 +105,7 @@ async def release(self) -> None:
100105
await self._database._pool.release(self._connection)
101106
self._connection = None
102107

103-
async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]:
108+
async def fetch_all(self, query: ClauseElement) -> typing.List[Record]:
104109
assert self._connection is not None, "Connection is not acquired"
105110
query_str, args, context = self._compile(query)
106111
async with self._connection.cursor() as cursor:
@@ -121,7 +126,7 @@ async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]:
121126
finally:
122127
await cursor.close()
123128

124-
async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]:
129+
async def fetch_one(self, query: ClauseElement) -> typing.Optional[Record]:
125130
assert self._connection is not None, "Connection is not acquired"
126131
query_str, args, context = self._compile(query)
127132
async with self._connection.cursor() as cursor:

databases/backends/mysql.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
from sqlalchemy.sql.ddl import DDLElement
1313

1414
from databases.core import LOG_EXTRA, DatabaseURL
15-
from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend
15+
from databases.interfaces import (
16+
ConnectionBackend,
17+
DatabaseBackend,
18+
Record,
19+
TransactionBackend,
20+
)
1621

1722
logger = logging.getLogger("databases")
1823

@@ -100,7 +105,7 @@ async def release(self) -> None:
100105
await self._database._pool.release(self._connection)
101106
self._connection = None
102107

103-
async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]:
108+
async def fetch_all(self, query: ClauseElement) -> typing.List[Record]:
104109
assert self._connection is not None, "Connection is not acquired"
105110
query_str, args, context = self._compile(query)
106111
cursor = await self._connection.cursor()
@@ -121,7 +126,7 @@ async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]:
121126
finally:
122127
await cursor.close()
123128

124-
async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]:
129+
async def fetch_one(self, query: ClauseElement) -> typing.Optional[Record]:
125130
assert self._connection is not None, "Connection is not acquired"
126131
query_str, args, context = self._compile(query)
127132
cursor = await self._connection.cursor()

databases/backends/postgres.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
from sqlalchemy.types import TypeEngine
1212

1313
from databases.core import LOG_EXTRA, DatabaseURL
14-
from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend
14+
from databases.interfaces import (
15+
ConnectionBackend,
16+
DatabaseBackend,
17+
Record as RecordInterface,
18+
TransactionBackend,
19+
)
1520

1621
logger = logging.getLogger("databases")
1722

@@ -78,7 +83,7 @@ def connection(self) -> "PostgresConnection":
7883
return PostgresConnection(self, self._dialect)
7984

8085

81-
class Record(Sequence):
86+
class Record(RecordInterface):
8287
__slots__ = (
8388
"_row",
8489
"_result_columns",
@@ -105,7 +110,7 @@ def __init__(
105110
self._column_map, self._column_map_int, self._column_map_full = column_maps
106111

107112
@property
108-
def _mapping(self) -> asyncpg.Record:
113+
def _mapping(self) -> typing.Mapping:
109114
return self._row
110115

111116
def keys(self) -> typing.KeysView:
@@ -171,15 +176,15 @@ async def release(self) -> None:
171176
self._connection = await self._database._pool.release(self._connection)
172177
self._connection = None
173178

174-
async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]:
179+
async def fetch_all(self, query: ClauseElement) -> typing.List[RecordInterface]:
175180
assert self._connection is not None, "Connection is not acquired"
176181
query_str, args, result_columns = self._compile(query)
177182
rows = await self._connection.fetch(query_str, *args)
178183
dialect = self._dialect
179184
column_maps = self._create_column_maps(result_columns)
180185
return [Record(row, result_columns, dialect, column_maps) for row in rows]
181186

182-
async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]:
187+
async def fetch_one(self, query: ClauseElement) -> typing.Optional[RecordInterface]:
183188
assert self._connection is not None, "Connection is not acquired"
184189
query_str, args, result_columns = self._compile(query)
185190
row = await self._connection.fetchrow(query_str, *args)

databases/backends/sqlite.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
from sqlalchemy.sql.ddl import DDLElement
1212

1313
from databases.core import LOG_EXTRA, DatabaseURL
14-
from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend
14+
from databases.interfaces import (
15+
ConnectionBackend,
16+
DatabaseBackend,
17+
Record,
18+
TransactionBackend,
19+
)
1520

1621
logger = logging.getLogger("databases")
1722

@@ -86,7 +91,7 @@ async def release(self) -> None:
8691
await self._pool.release(self._connection)
8792
self._connection = None
8893

89-
async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]:
94+
async def fetch_all(self, query: ClauseElement) -> typing.List[Record]:
9095
assert self._connection is not None, "Connection is not acquired"
9196
query_str, args, context = self._compile(query)
9297

@@ -104,7 +109,7 @@ async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]:
104109
for row in rows
105110
]
106111

107-
async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]:
112+
async def fetch_one(self, query: ClauseElement) -> typing.Optional[Record]:
108113
assert self._connection is not None, "Connection is not acquired"
109114
query_str, args, context = self._compile(query)
110115

databases/core.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
from sqlalchemy.sql import ClauseElement
1212

1313
from databases.importer import import_from_string
14-
from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend
14+
from databases.interfaces import (
15+
ConnectionBackend,
16+
DatabaseBackend,
17+
Record,
18+
TransactionBackend,
19+
)
1520

1621
if sys.version_info >= (3, 7): # pragma: no cover
1722
import contextvars as contextvars
@@ -144,13 +149,13 @@ async def __aexit__(
144149

145150
async def fetch_all(
146151
self, query: typing.Union[ClauseElement, str], values: dict = None
147-
) -> typing.List[typing.Sequence]:
152+
) -> typing.List[Record]:
148153
async with self.connection() as connection:
149154
return await connection.fetch_all(query, values)
150155

151156
async def fetch_one(
152157
self, query: typing.Union[ClauseElement, str], values: dict = None
153-
) -> typing.Optional[typing.Sequence]:
158+
) -> typing.Optional[Record]:
154159
async with self.connection() as connection:
155160
return await connection.fetch_one(query, values)
156161

@@ -265,14 +270,14 @@ async def __aexit__(
265270

266271
async def fetch_all(
267272
self, query: typing.Union[ClauseElement, str], values: dict = None
268-
) -> typing.List[typing.Sequence]:
273+
) -> typing.List[Record]:
269274
built_query = self._build_query(query, values)
270275
async with self._query_lock:
271276
return await self._connection.fetch_all(built_query)
272277

273278
async def fetch_one(
274279
self, query: typing.Union[ClauseElement, str], values: dict = None
275-
) -> typing.Optional[typing.Sequence]:
280+
) -> typing.Optional[Record]:
276281
built_query = self._build_query(query, values)
277282
async with self._query_lock:
278283
return await self._connection.fetch_one(built_query)

databases/interfaces.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import typing
2+
from collections.abc import Sequence
23

34
from sqlalchemy.sql import ClauseElement
45

@@ -21,10 +22,10 @@ async def acquire(self) -> None:
2122
async def release(self) -> None:
2223
raise NotImplementedError() # pragma: no cover
2324

24-
async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]:
25+
async def fetch_all(self, query: ClauseElement) -> typing.List["Record"]:
2526
raise NotImplementedError() # pragma: no cover
2627

27-
async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]:
28+
async def fetch_one(self, query: ClauseElement) -> typing.Optional["Record"]:
2829
raise NotImplementedError() # pragma: no cover
2930

3031
async def fetch_val(
@@ -66,3 +67,9 @@ async def commit(self) -> None:
6667

6768
async def rollback(self) -> None:
6869
raise NotImplementedError() # pragma: no cover
70+
71+
72+
class Record(Sequence):
73+
@property
74+
def _mapping(self) -> typing.Mapping:
75+
raise NotImplementedError() # pragma: no cover

docs/database_queries.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,20 @@ Note that query arguments should follow the `:query_arg` style.
108108

109109
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
110110
[sqlalchemy-core-tutorial]: https://docs.sqlalchemy.org/en/latest/core/tutorial.html
111+
112+
## Query result
113+
114+
To keep in line with [SQLAlchemy 1.4 changes][sqlalchemy-mapping-changes]
115+
query result object no longer implements a mapping interface.
116+
To access query result as a mapping you should use the `_mapping` property.
117+
That way you can process both SQLAlchemy Rows and databases Records from raw queries
118+
with the same function without any instance checks.
119+
120+
```python
121+
query = "SELECT * FROM notes WHERE id = :id"
122+
result = await database.fetch_one(query=query, values={"id": 1})
123+
result.id # access field via attribute
124+
result._mapping['id'] # access field via mapping
125+
```
126+
127+
[sqlalchemy-mapping-changes]: https://docs.sqlalchemy.org/en/14/changelog/migration_14.html#rowproxy-is-no-longer-a-proxy-is-now-called-row-and-behaves-like-an-enhanced-named-tuple

tests/test_databases.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,3 +1222,25 @@ async def test_result_named_access(database_url):
12221222

12231223
assert result.text == "example1"
12241224
assert result.completed is True
1225+
1226+
1227+
@pytest.mark.parametrize("database_url", DATABASE_URLS)
1228+
@mysql_versions
1229+
@async_adapter
1230+
async def test_mapping_property_interface(database_url):
1231+
"""
1232+
Test that all connections implement interface with `_mapping` property
1233+
"""
1234+
async with Database(database_url) as database:
1235+
query = notes.insert()
1236+
values = {"text": "example1", "completed": True}
1237+
await database.execute(query, values)
1238+
1239+
query = notes.select()
1240+
single_result = await database.fetch_one(query=query)
1241+
assert single_result._mapping["text"] == "example1"
1242+
assert single_result._mapping["completed"] is True
1243+
1244+
list_result = await database.fetch_all(query=query)
1245+
assert list_result[0]._mapping["text"] == "example1"
1246+
assert list_result[0]._mapping["completed"] is True

0 commit comments

Comments
 (0)