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

Commit 89efe60

Browse files
authored
switch to sqlalchemy 1.4 (#299)
* switch to sqlalchemy 1.4 * fix deprecation warning and add tests
1 parent b9f35e5 commit 89efe60

File tree

6 files changed

+155
-30
lines changed

6 files changed

+155
-30
lines changed

databases/backends/aiopg.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
import aiopg
88
from aiopg.sa.engine import APGCompiler_psycopg2
99
from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
10+
from sqlalchemy.engine.cursor import CursorResultMetaData
1011
from sqlalchemy.engine.interfaces import Dialect, ExecutionContext
11-
from sqlalchemy.engine.result import ResultMetaData, RowProxy
12+
from sqlalchemy.engine.result import Row
1213
from sqlalchemy.sql import ClauseElement
1314
from sqlalchemy.sql.ddl import DDLElement
14-
from sqlalchemy.types import TypeEngine
1515

1616
from databases.core import DatabaseURL
1717
from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend
@@ -119,9 +119,15 @@ async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Mapping]:
119119
try:
120120
await cursor.execute(query, args)
121121
rows = await cursor.fetchall()
122-
metadata = ResultMetaData(context, cursor.description)
122+
metadata = CursorResultMetaData(context, cursor.description)
123123
return [
124-
RowProxy(metadata, row, metadata._processors, metadata._keymap)
124+
Row(
125+
metadata,
126+
metadata._processors,
127+
metadata._keymap,
128+
Row._default_key_style,
129+
row,
130+
)
125131
for row in rows
126132
]
127133
finally:
@@ -136,8 +142,14 @@ async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Mappin
136142
row = await cursor.fetchone()
137143
if row is None:
138144
return None
139-
metadata = ResultMetaData(context, cursor.description)
140-
return RowProxy(metadata, row, metadata._processors, metadata._keymap)
145+
metadata = CursorResultMetaData(context, cursor.description)
146+
return Row(
147+
metadata,
148+
metadata._processors,
149+
metadata._keymap,
150+
Row._default_key_style,
151+
row,
152+
)
141153
finally:
142154
cursor.close()
143155

@@ -169,9 +181,15 @@ async def iterate(
169181
cursor = await self._connection.cursor()
170182
try:
171183
await cursor.execute(query, args)
172-
metadata = ResultMetaData(context, cursor.description)
184+
metadata = CursorResultMetaData(context, cursor.description)
173185
async for row in cursor:
174-
yield RowProxy(metadata, row, metadata._processors, metadata._keymap)
186+
yield Row(
187+
metadata,
188+
metadata._processors,
189+
metadata._keymap,
190+
Row._default_key_style,
191+
row,
192+
)
175193
finally:
176194
cursor.close()
177195

@@ -196,6 +214,7 @@ def _compile(
196214
compiled._result_columns,
197215
compiled._ordered_columns,
198216
compiled._textual_ordered_columns,
217+
compiled._loose_column_name_matching,
199218
)
200219
else:
201220
args = {}

databases/backends/mysql.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55

66
import aiomysql
77
from sqlalchemy.dialects.mysql import pymysql
8+
from sqlalchemy.engine.cursor import CursorResultMetaData
89
from sqlalchemy.engine.interfaces import Dialect, ExecutionContext
9-
from sqlalchemy.engine.result import ResultMetaData, RowProxy
10+
from sqlalchemy.engine.result import Row
1011
from sqlalchemy.sql import ClauseElement
1112
from sqlalchemy.sql.ddl import DDLElement
12-
from sqlalchemy.types import TypeEngine
1313

1414
from databases.core import LOG_EXTRA, DatabaseURL
1515
from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend
@@ -107,9 +107,15 @@ async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Mapping]:
107107
try:
108108
await cursor.execute(query, args)
109109
rows = await cursor.fetchall()
110-
metadata = ResultMetaData(context, cursor.description)
110+
metadata = CursorResultMetaData(context, cursor.description)
111111
return [
112-
RowProxy(metadata, row, metadata._processors, metadata._keymap)
112+
Row(
113+
metadata,
114+
metadata._processors,
115+
metadata._keymap,
116+
Row._default_key_style,
117+
row,
118+
)
113119
for row in rows
114120
]
115121
finally:
@@ -124,8 +130,14 @@ async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Mappin
124130
row = await cursor.fetchone()
125131
if row is None:
126132
return None
127-
metadata = ResultMetaData(context, cursor.description)
128-
return RowProxy(metadata, row, metadata._processors, metadata._keymap)
133+
metadata = CursorResultMetaData(context, cursor.description)
134+
return Row(
135+
metadata,
136+
metadata._processors,
137+
metadata._keymap,
138+
Row._default_key_style,
139+
row,
140+
)
129141
finally:
130142
await cursor.close()
131143

@@ -159,9 +171,15 @@ async def iterate(
159171
cursor = await self._connection.cursor()
160172
try:
161173
await cursor.execute(query, args)
162-
metadata = ResultMetaData(context, cursor.description)
174+
metadata = CursorResultMetaData(context, cursor.description)
163175
async for row in cursor:
164-
yield RowProxy(metadata, row, metadata._processors, metadata._keymap)
176+
yield Row(
177+
metadata,
178+
metadata._processors,
179+
metadata._keymap,
180+
Row._default_key_style,
181+
row,
182+
)
165183
finally:
166184
await cursor.close()
167185

@@ -186,6 +204,7 @@ def _compile(
186204
compiled._result_columns,
187205
compiled._ordered_columns,
188206
compiled._textual_ordered_columns,
207+
compiled._loose_column_name_matching,
189208
)
190209
else:
191210
args = {}

databases/backends/postgres.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,29 @@ def __init__(
104104
self._dialect = dialect
105105
self._column_map, self._column_map_int, self._column_map_full = column_maps
106106

107+
@property
108+
def _mapping(self) -> asyncpg.Record:
109+
return self._row
110+
111+
def keys(self) -> typing.KeysView:
112+
import warnings
113+
114+
warnings.warn(
115+
"The `Row.keys()` method is deprecated to mimic SQLAlchemy behaviour, "
116+
"use `Row._mapping.keys()` instead.",
117+
DeprecationWarning,
118+
)
119+
return self._mapping.keys()
120+
107121
def values(self) -> typing.ValuesView:
108-
return self._row.values()
122+
import warnings
123+
124+
warnings.warn(
125+
"The `Row.values()` method is deprecated to mimic SQLAlchemy behaviour, "
126+
"use `Row._mapping.values()` instead.",
127+
DeprecationWarning,
128+
)
129+
return self._mapping.values()
109130

110131
def __getitem__(self, key: typing.Any) -> typing.Any:
111132
if len(self._column_map) == 0: # raw query

databases/backends/sqlite.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
import aiosqlite
66
from sqlalchemy.dialects.sqlite import pysqlite
7+
from sqlalchemy.engine.cursor import CursorResultMetaData
78
from sqlalchemy.engine.interfaces import Dialect, ExecutionContext
8-
from sqlalchemy.engine.result import ResultMetaData, RowProxy
9+
from sqlalchemy.engine.result import Row
910
from sqlalchemy.sql import ClauseElement
1011
from sqlalchemy.sql.ddl import DDLElement
11-
from sqlalchemy.types import TypeEngine
1212

1313
from databases.core import LOG_EXTRA, DatabaseURL
1414
from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend
@@ -92,9 +92,15 @@ async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Mapping]:
9292

9393
async with self._connection.execute(query, args) as cursor:
9494
rows = await cursor.fetchall()
95-
metadata = ResultMetaData(context, cursor.description)
95+
metadata = CursorResultMetaData(context, cursor.description)
9696
return [
97-
RowProxy(metadata, row, metadata._processors, metadata._keymap)
97+
Row(
98+
metadata,
99+
metadata._processors,
100+
metadata._keymap,
101+
Row._default_key_style,
102+
row,
103+
)
98104
for row in rows
99105
]
100106

@@ -106,8 +112,14 @@ async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Mappin
106112
row = await cursor.fetchone()
107113
if row is None:
108114
return None
109-
metadata = ResultMetaData(context, cursor.description)
110-
return RowProxy(metadata, row, metadata._processors, metadata._keymap)
115+
metadata = CursorResultMetaData(context, cursor.description)
116+
return Row(
117+
metadata,
118+
metadata._processors,
119+
metadata._keymap,
120+
Row._default_key_style,
121+
row,
122+
)
111123

112124
async def execute(self, query: ClauseElement) -> typing.Any:
113125
assert self._connection is not None, "Connection is not acquired"
@@ -129,9 +141,15 @@ async def iterate(
129141
assert self._connection is not None, "Connection is not acquired"
130142
query, args, context = self._compile(query)
131143
async with self._connection.execute(query, args) as cursor:
132-
metadata = ResultMetaData(context, cursor.description)
144+
metadata = CursorResultMetaData(context, cursor.description)
133145
async for row in cursor:
134-
yield RowProxy(metadata, row, metadata._processors, metadata._keymap)
146+
yield Row(
147+
metadata,
148+
metadata._processors,
149+
metadata._keymap,
150+
Row._default_key_style,
151+
row,
152+
)
135153

136154
def transaction(self) -> TransactionBackend:
137155
return SQLiteTransaction(self)
@@ -158,6 +176,7 @@ def _compile(
158176
compiled._result_columns,
159177
compiled._ordered_columns,
160178
compiled._textual_ordered_columns,
179+
compiled._loose_column_name_matching,
161180
)
162181

163182
query_message = compiled.string.replace(" \n", " ").replace("\n", " ")

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def get_packages(package):
4848
packages=get_packages("databases"),
4949
package_data={"databases": ["py.typed"]},
5050
data_files=[("", ["LICENSE.md"])],
51-
install_requires=['sqlalchemy<1.4', 'aiocontextvars;python_version<"3.7"'],
51+
install_requires=['sqlalchemy>=1.4,<1.5', 'aiocontextvars;python_version<"3.7"'],
5252
extras_require={
5353
"postgresql": ["asyncpg"],
5454
"mysql": ["aiomysql"],

tests/test_databases.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import decimal
44
import functools
55
import os
6+
import re
67

78
import pytest
89
import sqlalchemy
@@ -336,8 +337,8 @@ async def test_result_values_allow_duplicate_names(database_url):
336337
query = "SELECT 1 AS id, 2 AS id"
337338
row = await database.fetch_one(query=query)
338339

339-
assert list(row.keys()) == ["id", "id"]
340-
assert list(row.values()) == [1, 2]
340+
assert list(row._mapping.keys()) == ["id", "id"]
341+
assert list(row._mapping.values()) == [1, 2]
341342

342343

343344
@pytest.mark.parametrize("database_url", DATABASE_URLS)
@@ -981,7 +982,7 @@ async def test_iterate_outside_transaction_with_temp_table(database_url):
981982
@async_adapter
982983
async def test_column_names(database_url, select_query):
983984
"""
984-
Test that column names are exposed correctly through `.keys()` on each row.
985+
Test that column names are exposed correctly through `._mapping.keys()` on each row.
985986
"""
986987
async with Database(database_url) as database:
987988
async with database.transaction(force_rollback=True):
@@ -993,7 +994,7 @@ async def test_column_names(database_url, select_query):
993994
results = await database.fetch_all(query=select_query)
994995
assert len(results) == 1
995996

996-
assert sorted(results[0].keys()) == ["completed", "id", "text"]
997+
assert sorted(results[0]._mapping.keys()) == ["completed", "id", "text"]
997998
assert results[0]["text"] == "example1"
998999
assert results[0]["completed"] == True
9991000

@@ -1014,3 +1015,49 @@ async def test_task(db):
10141015

10151016
tasks = [test_task(database) for i in range(4)]
10161017
await asyncio.gather(*tasks)
1018+
1019+
1020+
@pytest.mark.parametrize("database_url", DATABASE_URLS)
1021+
@async_adapter
1022+
async def test_posgres_interface(database_url):
1023+
"""
1024+
Since SQLAlchemy 1.4, `Row.values()` is removed and `Row.keys()` is deprecated.
1025+
Custom postgres interface mimics more or less this behaviour by deprecating those
1026+
two methods
1027+
"""
1028+
database_url = DatabaseURL(database_url)
1029+
1030+
if database_url.scheme != "postgresql":
1031+
pytest.skip("Test is only for postgresql")
1032+
1033+
async with Database(database_url) as database:
1034+
async with database.transaction(force_rollback=True):
1035+
query = notes.insert()
1036+
values = {"text": "example1", "completed": True}
1037+
await database.execute(query, values)
1038+
1039+
query = notes.select()
1040+
result = await database.fetch_one(query=query)
1041+
1042+
with pytest.warns(
1043+
DeprecationWarning,
1044+
match=re.escape(
1045+
"The `Row.keys()` method is deprecated to mimic SQLAlchemy behaviour, "
1046+
"use `Row._mapping.keys()` instead."
1047+
),
1048+
):
1049+
assert (
1050+
list(result.keys())
1051+
== [k for k in result]
1052+
== ["id", "text", "completed"]
1053+
)
1054+
1055+
with pytest.warns(
1056+
DeprecationWarning,
1057+
match=re.escape(
1058+
"The `Row.values()` method is deprecated to mimic SQLAlchemy behaviour, "
1059+
"use `Row._mapping.values()` instead."
1060+
),
1061+
):
1062+
# avoid checking `id` at index 0 since it may change depending on the launched tests
1063+
assert list(result.values())[1:] == ["example1", True]

0 commit comments

Comments
 (0)