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

Commit a08177b

Browse files
authored
Merge branch 'master' into version-0.6.0
2 parents b684c09 + 0fe0adb commit a08177b

File tree

13 files changed

+164
-60
lines changed

13 files changed

+164
-60
lines changed

README.md

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,22 @@ Databases is suitable for integrating against any async Web framework, such as [
2929
$ pip install databases
3030
```
3131

32-
You can install the required database drivers with:
33-
34-
```shell
35-
$ pip install databases[postgresql]
36-
$ pip install databases[mysql]
37-
$ pip install databases[sqlite]
38-
```
32+
Database drivers supported are:
3933

40-
Default driver support is provided using one of [asyncpg][asyncpg], [aiomysql][aiomysql], or [aiosqlite][aiosqlite].
34+
* [asyncpg][asyncpg]
35+
* [aiopg][aiopg]
36+
* [aiomysql][aiomysql]
37+
* [asyncmy][asyncmy]
38+
* [aiosqlite][aiosqlite]
4139

42-
You can also use other database drivers supported by `databases`:
40+
You can install the required database drivers with:
4341

44-
```shel
45-
$ pip install databases[postgresql+aiopg]
46-
$ pip install databases[mysql+asyncmy]
42+
```shell
43+
$ pip install databases[asyncpg]
44+
$ pip install databases[aiopg]
45+
$ pip install databases[aiomysql]
46+
$ pip install databases[asyncmy]
47+
$ pip install databases[aiosqlite]
4748
```
4849

4950
Note that if you are using any synchronous SQLAlchemy functions such as `engine.create_all()` or [alembic][alembic] migrations then you still have to install a synchronous DB driver: [psycopg2][psycopg2] for PostgreSQL and [pymysql][pymysql] for MySQL.
@@ -56,7 +57,7 @@ For this example we'll create a very simple SQLite database to run some
5657
queries against.
5758

5859
```shell
59-
$ pip install databases[sqlite]
60+
$ pip install databases[aiosqlite]
6061
$ pip install ipython
6162
```
6263

@@ -68,7 +69,7 @@ expressions directly from the console.
6869
```python
6970
# Create a database instance, and connect to it.
7071
from databases import Database
71-
database = Database('sqlite:///example.db')
72+
database = Database('sqlite+aiosqlite:///example.db')
7273
await database.connect()
7374

7475
# Create a table.
@@ -103,8 +104,10 @@ for examples of how to start using databases together with SQLAlchemy core expre
103104
[psycopg2]: https://www.psycopg.org/
104105
[pymysql]: https://github.com/PyMySQL/PyMySQL
105106
[asyncpg]: https://github.com/MagicStack/asyncpg
107+
[aiopg]: https://github.com/aio-libs/aiopg
106108
[aiomysql]: https://github.com/aio-libs/aiomysql
107-
[aiosqlite]: https://github.com/jreese/aiosqlite
109+
[asyncmy]: https://github.com/long2ice/asyncmy
110+
[aiosqlite]: https://github.com/omnilib/aiosqlite
108111

109112
[starlette]: https://github.com/encode/starlette
110113
[sanic]: https://github.com/huge-success/sanic

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: 13 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:
@@ -150,6 +155,9 @@ def __iter__(self) -> typing.Iterator:
150155
def __len__(self) -> int:
151156
return len(self._row)
152157

158+
def __getattr__(self, name: str) -> typing.Any:
159+
return self._mapping.get(name)
160+
153161

154162
class PostgresConnection(ConnectionBackend):
155163
def __init__(self, database: PostgresBackend, dialect: Dialect):
@@ -168,15 +176,15 @@ async def release(self) -> None:
168176
self._connection = await self._database._pool.release(self._connection)
169177
self._connection = None
170178

171-
async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]:
179+
async def fetch_all(self, query: ClauseElement) -> typing.List[RecordInterface]:
172180
assert self._connection is not None, "Connection is not acquired"
173181
query_str, args, result_columns = self._compile(query)
174182
rows = await self._connection.fetch(query_str, *args)
175183
dialect = self._dialect
176184
column_maps = self._create_column_maps(result_columns)
177185
return [Record(row, result_columns, dialect, column_maps) for row in rows]
178186

179-
async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]:
187+
async def fetch_one(self, query: ClauseElement) -> typing.Optional[RecordInterface]:
180188
assert self._connection is not None, "Connection is not acquired"
181189
query_str, args, result_columns = self._compile(query)
182190
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/connections_and_transactions.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,17 @@ and for configuring the connection pool.
4444

4545
```python
4646
# Use an SSL connection.
47-
database = Database('postgresql://localhost/example?ssl=true')
47+
database = Database('postgresql+asyncpg://localhost/example?ssl=true')
4848

4949
# Use a connection pool of between 5-20 connections.
50-
database = Database('mysql://localhost/example?min_size=5&max_size=20')
50+
database = Database('mysql+aiomysql://localhost/example?min_size=5&max_size=20')
5151
```
5252

5353
You can also use keyword arguments to pass in any connection options.
5454
Available keyword arguments may differ between database backends.
5555

5656
```python
57-
database = Database('postgresql://localhost/example', ssl=True, min_size=5, max_size=20)
57+
database = Database('postgresql+asyncpg://localhost/example', ssl=True, min_size=5, max_size=20)
5858
```
5959

6060
## Transactions

docs/database_queries.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ You can now use any [SQLAlchemy core][sqlalchemy-core] queries ([official tutori
3434
```python
3535
from databases import Database
3636

37-
database = Database('postgresql://localhost/example')
37+
database = Database('postgresql+asyncpg://localhost/example')
3838

3939

4040
# Establish the connection pool
@@ -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

0 commit comments

Comments
 (0)