From 7e6b3197d6001bcda219b933f9660e547285bea9 Mon Sep 17 00:00:00 2001 From: Vladislav Laukhin Date: Wed, 19 Jan 2022 20:49:51 +0300 Subject: [PATCH 1/7] Add _mapping property to the returning Record interface. --- databases/backends/aiopg.py | 6 +++--- databases/backends/asyncmy.py | 6 +++--- databases/backends/mysql.py | 6 +++--- databases/backends/postgres.py | 6 +++--- databases/backends/sqlite.py | 6 +++--- databases/core.py | 10 +++++----- databases/interfaces.py | 11 +++++++++-- tests/test_databases.py | 22 ++++++++++++++++++++++ 8 files changed, 51 insertions(+), 22 deletions(-) diff --git a/databases/backends/aiopg.py b/databases/backends/aiopg.py index 1b73fa62..a04603db 100644 --- a/databases/backends/aiopg.py +++ b/databases/backends/aiopg.py @@ -14,7 +14,7 @@ from sqlalchemy.sql.ddl import DDLElement from databases.core import DatabaseURL -from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend +from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend, Record logger = logging.getLogger("databases") @@ -112,7 +112,7 @@ async def release(self) -> None: await self._database._pool.release(self._connection) self._connection = None - async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]: + async def fetch_all(self, query: ClauseElement) -> typing.List[Record]: assert self._connection is not None, "Connection is not acquired" query_str, args, context = self._compile(query) cursor = await self._connection.cursor() @@ -133,7 +133,7 @@ async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]: finally: cursor.close() - async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]: + async def fetch_one(self, query: ClauseElement) -> typing.Optional[Record]: assert self._connection is not None, "Connection is not acquired" query_str, args, context = self._compile(query) cursor = await self._connection.cursor() diff --git a/databases/backends/asyncmy.py b/databases/backends/asyncmy.py index c9b6611f..6654ce87 100644 --- a/databases/backends/asyncmy.py +++ b/databases/backends/asyncmy.py @@ -12,7 +12,7 @@ from sqlalchemy.sql.ddl import DDLElement from databases.core import LOG_EXTRA, DatabaseURL -from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend +from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend, Record logger = logging.getLogger("databases") @@ -100,7 +100,7 @@ async def release(self) -> None: await self._database._pool.release(self._connection) self._connection = None - async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]: + async def fetch_all(self, query: ClauseElement) -> typing.List[Record]: assert self._connection is not None, "Connection is not acquired" query_str, args, context = self._compile(query) async with self._connection.cursor() as cursor: @@ -121,7 +121,7 @@ async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]: finally: await cursor.close() - async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]: + async def fetch_one(self, query: ClauseElement) -> typing.Optional[Record]: assert self._connection is not None, "Connection is not acquired" query_str, args, context = self._compile(query) async with self._connection.cursor() as cursor: diff --git a/databases/backends/mysql.py b/databases/backends/mysql.py index 4c490d71..f64ec3a3 100644 --- a/databases/backends/mysql.py +++ b/databases/backends/mysql.py @@ -12,7 +12,7 @@ from sqlalchemy.sql.ddl import DDLElement from databases.core import LOG_EXTRA, DatabaseURL -from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend +from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend, Record logger = logging.getLogger("databases") @@ -100,7 +100,7 @@ async def release(self) -> None: await self._database._pool.release(self._connection) self._connection = None - async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]: + async def fetch_all(self, query: ClauseElement) -> typing.List[Record]: assert self._connection is not None, "Connection is not acquired" query_str, args, context = self._compile(query) cursor = await self._connection.cursor() @@ -121,7 +121,7 @@ async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]: finally: await cursor.close() - async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]: + async def fetch_one(self, query: ClauseElement) -> typing.Optional[Record]: assert self._connection is not None, "Connection is not acquired" query_str, args, context = self._compile(query) cursor = await self._connection.cursor() diff --git a/databases/backends/postgres.py b/databases/backends/postgres.py index ed12c2b0..de317cca 100644 --- a/databases/backends/postgres.py +++ b/databases/backends/postgres.py @@ -11,7 +11,7 @@ from sqlalchemy.types import TypeEngine from databases.core import LOG_EXTRA, DatabaseURL -from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend +from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend, Record as RecordInterface logger = logging.getLogger("databases") @@ -168,7 +168,7 @@ async def release(self) -> None: self._connection = await self._database._pool.release(self._connection) self._connection = None - async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]: + async def fetch_all(self, query: ClauseElement) -> typing.List[RecordInterface]: assert self._connection is not None, "Connection is not acquired" query_str, args, result_columns = self._compile(query) rows = await self._connection.fetch(query_str, *args) @@ -176,7 +176,7 @@ async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]: column_maps = self._create_column_maps(result_columns) return [Record(row, result_columns, dialect, column_maps) for row in rows] - async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]: + async def fetch_one(self, query: ClauseElement) -> typing.Optional[RecordInterface]: assert self._connection is not None, "Connection is not acquired" query_str, args, result_columns = self._compile(query) row = await self._connection.fetchrow(query_str, *args) diff --git a/databases/backends/sqlite.py b/databases/backends/sqlite.py index 46a39519..6ae5d3f8 100644 --- a/databases/backends/sqlite.py +++ b/databases/backends/sqlite.py @@ -11,7 +11,7 @@ from sqlalchemy.sql.ddl import DDLElement from databases.core import LOG_EXTRA, DatabaseURL -from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend +from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend, Record logger = logging.getLogger("databases") @@ -86,7 +86,7 @@ async def release(self) -> None: await self._pool.release(self._connection) self._connection = None - async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]: + async def fetch_all(self, query: ClauseElement) -> typing.List[Record]: assert self._connection is not None, "Connection is not acquired" query_str, args, context = self._compile(query) @@ -104,7 +104,7 @@ async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]: for row in rows ] - async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]: + async def fetch_one(self, query: ClauseElement) -> typing.Optional[Record]: assert self._connection is not None, "Connection is not acquired" query_str, args, context = self._compile(query) diff --git a/databases/core.py b/databases/core.py index b3c2b440..6552499c 100644 --- a/databases/core.py +++ b/databases/core.py @@ -11,7 +11,7 @@ from sqlalchemy.sql import ClauseElement from databases.importer import import_from_string -from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend +from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend, Record if sys.version_info >= (3, 7): # pragma: no cover import contextvars as contextvars @@ -144,13 +144,13 @@ async def __aexit__( async def fetch_all( self, query: typing.Union[ClauseElement, str], values: dict = None - ) -> typing.List[typing.Sequence]: + ) -> typing.List[Record]: async with self.connection() as connection: return await connection.fetch_all(query, values) async def fetch_one( self, query: typing.Union[ClauseElement, str], values: dict = None - ) -> typing.Optional[typing.Sequence]: + ) -> typing.Optional[Record]: async with self.connection() as connection: return await connection.fetch_one(query, values) @@ -265,14 +265,14 @@ async def __aexit__( async def fetch_all( self, query: typing.Union[ClauseElement, str], values: dict = None - ) -> typing.List[typing.Sequence]: + ) -> typing.List[Record]: built_query = self._build_query(query, values) async with self._query_lock: return await self._connection.fetch_all(built_query) async def fetch_one( self, query: typing.Union[ClauseElement, str], values: dict = None - ) -> typing.Optional[typing.Sequence]: + ) -> typing.Optional[Record]: built_query = self._build_query(query, values) async with self._query_lock: return await self._connection.fetch_one(built_query) diff --git a/databases/interfaces.py b/databases/interfaces.py index 9bf24435..5ccf3e8d 100644 --- a/databases/interfaces.py +++ b/databases/interfaces.py @@ -1,4 +1,5 @@ import typing +from collections.abc import Sequence from sqlalchemy.sql import ClauseElement @@ -21,10 +22,10 @@ async def acquire(self) -> None: async def release(self) -> None: raise NotImplementedError() # pragma: no cover - async def fetch_all(self, query: ClauseElement) -> typing.List[typing.Sequence]: + async def fetch_all(self, query: ClauseElement) -> typing.List['Record']: raise NotImplementedError() # pragma: no cover - async def fetch_one(self, query: ClauseElement) -> typing.Optional[typing.Sequence]: + async def fetch_one(self, query: ClauseElement) -> typing.Optional['Record']: raise NotImplementedError() # pragma: no cover async def fetch_val( @@ -66,3 +67,9 @@ async def commit(self) -> None: async def rollback(self) -> None: raise NotImplementedError() # pragma: no cover + + +class Record(Sequence): + @property + def _mapping(self) -> typing.Mapping: + raise NotImplementedError() # pragma: no cover diff --git a/tests/test_databases.py b/tests/test_databases.py index 8c99f547..56be1c66 100644 --- a/tests/test_databases.py +++ b/tests/test_databases.py @@ -1206,3 +1206,25 @@ async def test_postcompile_queries(database_url): results = await database.fetch_all(query=query) assert len(results) == 0 + + +@pytest.mark.parametrize("database_url", DATABASE_URLS) +@mysql_versions +@async_adapter +async def test_mapping_property_interface(database_url): + """ + Test that all connections implements interface with `_mapping` property + """ + async with Database(database_url) as database: + query = notes.insert() + values = {"text": "example1", "completed": True} + await database.execute(query, values) + + query = notes.select() + single_result = await database.fetch_one(query=query) + assert single_result._mapping["text"] == "example1" + assert single_result._mapping["completed"] is True + + list_result = await database.fetch_all(query=query) + assert list_result[0]._mapping["text"] == "example1" + assert list_result[0]._mapping["completed"] is True From 7ed0a48542e0ee06feddddfa46a41fb87a48c138 Mon Sep 17 00:00:00 2001 From: Vladislav Laukhin Date: Wed, 19 Jan 2022 21:28:44 +0300 Subject: [PATCH 2/7] Blackify and isortify. --- databases/backends/aiopg.py | 7 ++++++- databases/backends/asyncmy.py | 7 ++++++- databases/backends/mysql.py | 7 ++++++- databases/backends/postgres.py | 7 ++++++- databases/backends/sqlite.py | 7 ++++++- databases/core.py | 7 ++++++- databases/interfaces.py | 4 ++-- 7 files changed, 38 insertions(+), 8 deletions(-) diff --git a/databases/backends/aiopg.py b/databases/backends/aiopg.py index a04603db..9ad12f63 100644 --- a/databases/backends/aiopg.py +++ b/databases/backends/aiopg.py @@ -14,7 +14,12 @@ from sqlalchemy.sql.ddl import DDLElement from databases.core import DatabaseURL -from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend, Record +from databases.interfaces import ( + ConnectionBackend, + DatabaseBackend, + Record, + TransactionBackend, +) logger = logging.getLogger("databases") diff --git a/databases/backends/asyncmy.py b/databases/backends/asyncmy.py index 6654ce87..e15dfa45 100644 --- a/databases/backends/asyncmy.py +++ b/databases/backends/asyncmy.py @@ -12,7 +12,12 @@ from sqlalchemy.sql.ddl import DDLElement from databases.core import LOG_EXTRA, DatabaseURL -from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend, Record +from databases.interfaces import ( + ConnectionBackend, + DatabaseBackend, + Record, + TransactionBackend, +) logger = logging.getLogger("databases") diff --git a/databases/backends/mysql.py b/databases/backends/mysql.py index f64ec3a3..2a0a8425 100644 --- a/databases/backends/mysql.py +++ b/databases/backends/mysql.py @@ -12,7 +12,12 @@ from sqlalchemy.sql.ddl import DDLElement from databases.core import LOG_EXTRA, DatabaseURL -from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend, Record +from databases.interfaces import ( + ConnectionBackend, + DatabaseBackend, + Record, + TransactionBackend, +) logger = logging.getLogger("databases") diff --git a/databases/backends/postgres.py b/databases/backends/postgres.py index de317cca..4912312f 100644 --- a/databases/backends/postgres.py +++ b/databases/backends/postgres.py @@ -11,7 +11,12 @@ from sqlalchemy.types import TypeEngine from databases.core import LOG_EXTRA, DatabaseURL -from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend, Record as RecordInterface +from databases.interfaces import ( + ConnectionBackend, + DatabaseBackend, + Record as RecordInterface, + TransactionBackend, +) logger = logging.getLogger("databases") diff --git a/databases/backends/sqlite.py b/databases/backends/sqlite.py index 6ae5d3f8..9626dcf8 100644 --- a/databases/backends/sqlite.py +++ b/databases/backends/sqlite.py @@ -11,7 +11,12 @@ from sqlalchemy.sql.ddl import DDLElement from databases.core import LOG_EXTRA, DatabaseURL -from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend, Record +from databases.interfaces import ( + ConnectionBackend, + DatabaseBackend, + Record, + TransactionBackend, +) logger = logging.getLogger("databases") diff --git a/databases/core.py b/databases/core.py index 6552499c..893eb37e 100644 --- a/databases/core.py +++ b/databases/core.py @@ -11,7 +11,12 @@ from sqlalchemy.sql import ClauseElement from databases.importer import import_from_string -from databases.interfaces import ConnectionBackend, DatabaseBackend, TransactionBackend, Record +from databases.interfaces import ( + ConnectionBackend, + DatabaseBackend, + Record, + TransactionBackend, +) if sys.version_info >= (3, 7): # pragma: no cover import contextvars as contextvars diff --git a/databases/interfaces.py b/databases/interfaces.py index 5ccf3e8d..c2109a23 100644 --- a/databases/interfaces.py +++ b/databases/interfaces.py @@ -22,10 +22,10 @@ async def acquire(self) -> None: async def release(self) -> None: raise NotImplementedError() # pragma: no cover - async def fetch_all(self, query: ClauseElement) -> typing.List['Record']: + async def fetch_all(self, query: ClauseElement) -> typing.List["Record"]: raise NotImplementedError() # pragma: no cover - async def fetch_one(self, query: ClauseElement) -> typing.Optional['Record']: + async def fetch_one(self, query: ClauseElement) -> typing.Optional["Record"]: raise NotImplementedError() # pragma: no cover async def fetch_val( From 6af2e09e65c2a21f6379354269716dc0c987a9a1 Mon Sep 17 00:00:00 2001 From: Vladislav Laukhin Date: Wed, 19 Jan 2022 21:35:59 +0300 Subject: [PATCH 3/7] Fix mypy issues for the postgres Record interface. --- databases/backends/postgres.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/databases/backends/postgres.py b/databases/backends/postgres.py index 4912312f..09820307 100644 --- a/databases/backends/postgres.py +++ b/databases/backends/postgres.py @@ -83,7 +83,7 @@ def connection(self) -> "PostgresConnection": return PostgresConnection(self, self._dialect) -class Record(Sequence): +class Record(RecordInterface): __slots__ = ( "_row", "_result_columns", @@ -110,7 +110,7 @@ def __init__( self._column_map, self._column_map_int, self._column_map_full = column_maps @property - def _mapping(self) -> asyncpg.Record: + def _mapping(self) -> typing.Mapping: return self._row def keys(self) -> typing.KeysView: From a5d4a91702485cf3157495a574121cb3ab57cf7d Mon Sep 17 00:00:00 2001 From: collerek Date: Thu, 20 Jan 2022 19:03:48 +0100 Subject: [PATCH 4/7] Fix black (remind myself to not use web ide ;O) --- tests/test_databases.py | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/tests/test_databases.py b/tests/test_databases.py index a487382d..5f91aa37 100644 --- a/tests/test_databases.py +++ b/tests/test_databases.py @@ -1211,40 +1211,14 @@ async def test_postcompile_queries(database_url): @pytest.mark.parametrize("database_url", DATABASE_URLS) @mysql_versions @async_adapter -async def test_mapping_property_interface(database_url): - """ - Test that all connections implements interface with `_mapping` property - """ +async def test_result_named_access(database_url): async with Database(database_url) as database: query = notes.insert() values = {"text": "example1", "completed": True} await database.execute(query, values) - - query = notes.select() - single_result = await database.fetch_one(query=query) - assert single_result._mapping["text"] == "example1" - assert single_result._mapping["completed"] is True - list_result = await database.fetch_all(query=query) - assert list_result[0]._mapping["text"] == "example1" - assert list_result[0]._mapping["completed"] is True - - -@pytest.mark.parametrize("database_url", DATABASE_URLS) -@mysql_versions -@async_adapter -async def test_result_named_access(database_url): - """ - Test attribute access of result columns - """ - async with Database(database_url) as database: - query = notes.insert() - values = {"text": "example1", "completed": True} - await database.execute(query, values) - query = notes.select().where(notes.c.text == "example1") result = await database.fetch_one(query=query) assert result.text == "example1" assert result.completed is True - From 5067ed1725dbf25f78628426d86a016ddb32706b Mon Sep 17 00:00:00 2001 From: collerek Date: Thu, 20 Jan 2022 19:10:52 +0100 Subject: [PATCH 5/7] WTF is wrong with this github ide :D Now should be fixed --- tests/test_databases.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_databases.py b/tests/test_databases.py index 5f91aa37..ad7084d8 100644 --- a/tests/test_databases.py +++ b/tests/test_databases.py @@ -1222,3 +1222,25 @@ async def test_result_named_access(database_url): assert result.text == "example1" assert result.completed is True + + +@pytest.mark.parametrize("database_url", DATABASE_URLS) +@mysql_versions +@async_adapter +async def test_mapping_property_interface(database_url): + """ + Test that all connections implements interface with `_mapping` property + """ + async with Database(database_url) as database: + query = notes.insert() + values = {"text": "example1", "completed": True} + await database.execute(query, values) + + query = notes.select() + single_result = await database.fetch_one(query=query) + assert single_result._mapping["text"] == "example1" + assert single_result._mapping["completed"] is True + + list_result = await database.fetch_all(query=query) + assert list_result[0]._mapping["text"] == "example1" + assert list_result[0]._mapping["completed"] is True From 8a7dec616de0a7d912911e288ecf0f259b143d06 Mon Sep 17 00:00:00 2001 From: Vladislav Laukhin Date: Tue, 25 Jan 2022 20:43:20 +0300 Subject: [PATCH 6/7] Document _mapping property. --- docs/database_queries.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/database_queries.md b/docs/database_queries.md index 11721237..898e7343 100644 --- a/docs/database_queries.md +++ b/docs/database_queries.md @@ -108,3 +108,20 @@ Note that query arguments should follow the `:query_arg` style. [sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/ [sqlalchemy-core-tutorial]: https://docs.sqlalchemy.org/en/latest/core/tutorial.html + +## Query result + +To keep in line with [SQLAlchemy 1.4 changes][sqlalchemy-mapping-changes] +query result object no longer implements a mapping interface. +To access query result as a mapping you should use the `_mapping` property. +That way you can process both SQLAlchemy Rows and databases Records from raw queries +with the same function without any instance checks. + +```python +query = "SELECT * FROM notes WHERE id = :id" +result = await database.fetch_one(query=query, values={"id": 1}) +result.id # access field via attribute +result._mapping['id'] # access field via mapping +``` + +[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 From ccbb808b5917d7fbf85b0978d0560d05772aa8f5 Mon Sep 17 00:00:00 2001 From: Vladislav Laukhin <38098983+laukhin@users.noreply.github.com> Date: Wed, 26 Jan 2022 02:55:27 +0300 Subject: [PATCH 7/7] Fix grammar issue for docstring. Co-authored-by: Amin Alaee --- tests/test_databases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_databases.py b/tests/test_databases.py index ad7084d8..ad1fb542 100644 --- a/tests/test_databases.py +++ b/tests/test_databases.py @@ -1229,7 +1229,7 @@ async def test_result_named_access(database_url): @async_adapter async def test_mapping_property_interface(database_url): """ - Test that all connections implements interface with `_mapping` property + Test that all connections implement interface with `_mapping` property """ async with Database(database_url) as database: query = notes.insert()