diff --git a/docs/usage/drivers_and_querying.rst b/docs/usage/drivers_and_querying.rst index 481588d29..621c03b55 100644 --- a/docs/usage/drivers_and_querying.rst +++ b/docs/usage/drivers_and_querying.rst @@ -388,6 +388,52 @@ Execute a SELECT query returning a single scalar value. :dedent: 4 :caption: `select_value` +asyncpg-Compatible Method Aliases (fetch*) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For users transitioning from asyncpg or preferring its naming convention, SQLSpec provides ``fetch*`` aliases for all ``select*`` methods. Both naming styles are fully supported and produce identical results. + +.. list-table:: Method Aliases + :header-rows: 1 + :widths: 40 60 + + * - Primary Method + - asyncpg-Compatible Alias + * - ``select()`` + - ``fetch()`` + * - ``select_one()`` + - ``fetch_one()`` + * - ``select_one_or_none()`` + - ``fetch_one_or_none()`` + * - ``select_value()`` + - ``fetch_value()`` + * - ``select_value_or_none()`` + - ``fetch_value_or_none()`` + * - ``select_to_arrow()`` + - ``fetch_to_arrow()`` + * - ``select_with_total()`` + - ``fetch_with_total()`` + +**Example using fetch() instead of select():** + +.. code-block:: python + + # Both styles work identically + async with spec.provide_session(config) as session: + # Primary naming (recommended in docs) + users = await session.select("SELECT * FROM users WHERE age > ?", 18) + + # asyncpg-compatible naming (identical behavior) + users = await session.fetch("SELECT * FROM users WHERE age > ?", 18) + + # fetch_one() is equivalent to select_one() + user = await session.fetch_one("SELECT * FROM users WHERE id = ?", 1) + + # fetch_value() is equivalent to select_value() + count = await session.fetch_value("SELECT COUNT(*) FROM users") + +**Note:** Both naming conventions are fully supported and will not be deprecated. Choose the style that feels most natural for your codebase. The SQLSpec documentation primarily uses ``select*`` methods, but ``fetch*`` aliases are equally valid. + Working with Results -------------------- diff --git a/sqlspec/driver/_async.py b/sqlspec/driver/_async.py index 399b2e488..be99ad559 100644 --- a/sqlspec/driver/_async.py +++ b/sqlspec/driver/_async.py @@ -398,6 +398,51 @@ async def select_one( except ValueError as error: handle_single_row_error(error) + @overload + async def fetch_one( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT]", + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "SchemaT": ... + + @overload + async def fetch_one( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: None = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "dict[str, Any]": ... + + async def fetch_one( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT] | None" = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "SchemaT | dict[str, Any]": + """Execute a select statement and return exactly one row. + + This is an alias for :meth:`select_one` provided for users familiar + with asyncpg's fetch_one() naming convention. + + Raises an exception if no rows or more than one row is returned. + + See Also: + select_one(): Primary method with identical behavior + """ + return await self.select_one( + statement, *parameters, schema_type=schema_type, statement_config=statement_config, **kwargs + ) + @overload async def select_one_or_none( self, @@ -437,6 +482,52 @@ async def select_one_or_none( result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs) return result.one_or_none(schema_type=schema_type) + @overload + async def fetch_one_or_none( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT]", + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "SchemaT | None": ... + + @overload + async def fetch_one_or_none( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: None = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "dict[str, Any] | None": ... + + async def fetch_one_or_none( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT] | None" = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "SchemaT | dict[str, Any] | None": + """Execute a select statement and return at most one row. + + This is an alias for :meth:`select_one_or_none` provided for users familiar + with asyncpg's fetch_one_or_none() naming convention. + + Returns None if no rows are found. + Raises an exception if more than one row is returned. + + See Also: + select_one_or_none(): Primary method with identical behavior + """ + return await self.select_one_or_none( + statement, *parameters, schema_type=schema_type, statement_config=statement_config, **kwargs + ) + @overload async def select( self, @@ -472,6 +563,49 @@ async def select( result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs) return result.get_data(schema_type=schema_type) + @overload + async def fetch( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT]", + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "list[SchemaT]": ... + + @overload + async def fetch( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: None = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "list[dict[str, Any]]": ... + + async def fetch( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT] | None" = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "list[SchemaT] | list[dict[str, Any]]": + """Execute a select statement and return all rows. + + This is an alias for :meth:`select` provided for users familiar + with asyncpg's fetch() naming convention. + + See Also: + select(): Primary method with identical behavior + """ + return await self.select( + statement, *parameters, schema_type=schema_type, statement_config=statement_config, **kwargs + ) + async def select_to_arrow( self, statement: "Statement | QueryBuilder", @@ -549,6 +683,37 @@ async def select_to_arrow( metadata=result.metadata, ) + async def fetch_to_arrow( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + statement_config: "StatementConfig | None" = None, + return_format: "ArrowReturnFormat" = "table", + native_only: bool = False, + batch_size: int | None = None, + arrow_schema: Any = None, + **kwargs: Any, + ) -> "ArrowResult": + """Execute query and return results as Apache Arrow format (async). + + This is an alias for :meth:`select_to_arrow` provided for users familiar + with asyncpg's fetch() naming convention. + + See Also: + select_to_arrow(): Primary method with identical behavior and full documentation + """ + return await self.select_to_arrow( + statement, + *parameters, + statement_config=statement_config, + return_format=return_format, + native_only=native_only, + batch_size=batch_size, + arrow_schema=arrow_schema, + **kwargs, + ) + async def select_value( self, statement: "Statement | QueryBuilder", @@ -568,6 +733,27 @@ async def select_value( except ValueError as error: handle_single_row_error(error) + async def fetch_value( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> Any: + """Execute a select statement and return a single scalar value. + + This is an alias for :meth:`select_value` provided for users familiar + with asyncpg's fetch_value() naming convention. + + Expects exactly one row with one column. + Raises an exception if no rows or more than one row/column is returned. + + See Also: + select_value(): Primary method with identical behavior + """ + return await self.select_value(statement, *parameters, statement_config=statement_config, **kwargs) + async def select_value_or_none( self, statement: "Statement | QueryBuilder", @@ -585,6 +771,28 @@ async def select_value_or_none( result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs) return result.scalar_or_none() + async def fetch_value_or_none( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> Any: + """Execute a select statement and return a single scalar value or None. + + This is an alias for :meth:`select_value_or_none` provided for users familiar + with asyncpg's fetch_value_or_none() naming convention. + + Returns None if no rows are found. + Expects at most one row with one column. + Raises an exception if more than one row is returned. + + See Also: + select_value_or_none(): Primary method with identical behavior + """ + return await self.select_value_or_none(statement, *parameters, statement_config=statement_config, **kwargs) + @overload async def select_with_total( self, @@ -641,6 +849,52 @@ async def select_with_total( return (select_result.get_data(schema_type=schema_type), count_result.scalar()) + @overload + async def fetch_with_total( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT]", + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "tuple[list[SchemaT], int]": ... + + @overload + async def fetch_with_total( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: None = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "tuple[list[dict[str, Any]], int]": ... + + async def fetch_with_total( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT] | None" = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "tuple[list[SchemaT] | list[dict[str, Any]], int]": + """Execute a select statement and return both the data and total count. + + This is an alias for :meth:`select_with_total` provided for users familiar + with asyncpg's fetch() naming convention. + + This method is designed for pagination scenarios where you need both + the current page of data and the total number of rows that match the query. + + See Also: + select_with_total(): Primary method with identical behavior and full documentation + """ + return await self.select_with_total( + statement, *parameters, schema_type=schema_type, statement_config=statement_config, **kwargs + ) + async def _execute_stack_operation(self, operation: "StackOperation") -> "SQLResult | ArrowResult | None": kwargs = dict(operation.keyword_arguments) if operation.keyword_arguments else {} diff --git a/sqlspec/driver/_sync.py b/sqlspec/driver/_sync.py index 146b7b864..7630a536d 100644 --- a/sqlspec/driver/_sync.py +++ b/sqlspec/driver/_sync.py @@ -396,6 +396,51 @@ def select_one( except ValueError as error: handle_single_row_error(error) + @overload + def fetch_one( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT]", + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "SchemaT": ... + + @overload + def fetch_one( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: None = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "dict[str, Any]": ... + + def fetch_one( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT] | None" = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "SchemaT | dict[str, Any]": + """Execute a select statement and return exactly one row. + + This is an alias for :meth:`select_one` provided for users familiar + with asyncpg's fetch_one() naming convention. + + Raises an exception if no rows or more than one row is returned. + + See Also: + select_one(): Primary method with identical behavior + """ + return self.select_one( + statement, *parameters, schema_type=schema_type, statement_config=statement_config, **kwargs + ) + @overload def select_one_or_none( self, @@ -435,6 +480,52 @@ def select_one_or_none( result = self.execute(statement, *parameters, statement_config=statement_config, **kwargs) return result.one_or_none(schema_type=schema_type) + @overload + def fetch_one_or_none( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT]", + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "SchemaT | None": ... + + @overload + def fetch_one_or_none( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: None = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "dict[str, Any] | None": ... + + def fetch_one_or_none( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT] | None" = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "SchemaT | dict[str, Any] | None": + """Execute a select statement and return at most one row. + + This is an alias for :meth:`select_one_or_none` provided for users familiar + with asyncpg's fetch_one_or_none() naming convention. + + Returns None if no rows are found. + Raises an exception if more than one row is returned. + + See Also: + select_one_or_none(): Primary method with identical behavior + """ + return self.select_one_or_none( + statement, *parameters, schema_type=schema_type, statement_config=statement_config, **kwargs + ) + @overload def select( self, @@ -470,6 +561,47 @@ def select( result = self.execute(statement, *parameters, statement_config=statement_config, **kwargs) return result.get_data(schema_type=schema_type) + @overload + def fetch( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT]", + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "list[SchemaT]": ... + + @overload + def fetch( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: None = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "list[dict[str, Any]]": ... + + def fetch( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT] | None" = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "list[SchemaT] | list[dict[str, Any]]": + """Execute a select statement and return all rows. + + This is an alias for :meth:`select` provided for users familiar + with asyncpg's fetch() naming convention. + + See Also: + select(): Primary method with identical behavior + """ + return self.select(statement, *parameters, schema_type=schema_type, statement_config=statement_config, **kwargs) + def select_to_arrow( self, statement: "Statement | QueryBuilder", @@ -549,6 +681,37 @@ def select_to_arrow( metadata=result.metadata, ) + def fetch_to_arrow( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + statement_config: "StatementConfig | None" = None, + return_format: "ArrowReturnFormat" = "table", + native_only: bool = False, + batch_size: int | None = None, + arrow_schema: Any = None, + **kwargs: Any, + ) -> "ArrowResult": + """Execute query and return results as Apache Arrow format. + + This is an alias for :meth:`select_to_arrow` provided for users familiar + with asyncpg's fetch() naming convention. + + See Also: + select_to_arrow(): Primary method with identical behavior and full documentation + """ + return self.select_to_arrow( + statement, + *parameters, + statement_config=statement_config, + return_format=return_format, + native_only=native_only, + batch_size=batch_size, + arrow_schema=arrow_schema, + **kwargs, + ) + def select_value( self, statement: "Statement | QueryBuilder", @@ -568,6 +731,27 @@ def select_value( except ValueError as error: handle_single_row_error(error) + def fetch_value( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> Any: + """Execute a select statement and return a single scalar value. + + This is an alias for :meth:`select_value` provided for users familiar + with asyncpg's fetch_value() naming convention. + + Expects exactly one row with one column. + Raises an exception if no rows or more than one row/column is returned. + + See Also: + select_value(): Primary method with identical behavior + """ + return self.select_value(statement, *parameters, statement_config=statement_config, **kwargs) + def select_value_or_none( self, statement: "Statement | QueryBuilder", @@ -585,6 +769,28 @@ def select_value_or_none( result = self.execute(statement, *parameters, statement_config=statement_config, **kwargs) return result.scalar_or_none() + def fetch_value_or_none( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> Any: + """Execute a select statement and return a single scalar value or None. + + This is an alias for :meth:`select_value_or_none` provided for users familiar + with asyncpg's fetch_value_or_none() naming convention. + + Returns None if no rows are found. + Expects at most one row with one column. + Raises an exception if more than one row is returned. + + See Also: + select_value_or_none(): Primary method with identical behavior + """ + return self.select_value_or_none(statement, *parameters, statement_config=statement_config, **kwargs) + @overload def select_with_total( self, @@ -641,6 +847,52 @@ def select_with_total( return (select_result.get_data(schema_type=schema_type), count_result.scalar()) + @overload + def fetch_with_total( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT]", + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "tuple[list[SchemaT], int]": ... + + @overload + def fetch_with_total( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: None = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "tuple[list[dict[str, Any]], int]": ... + + def fetch_with_total( + self, + statement: "Statement | QueryBuilder", + /, + *parameters: "StatementParameters | StatementFilter", + schema_type: "type[SchemaT] | None" = None, + statement_config: "StatementConfig | None" = None, + **kwargs: Any, + ) -> "tuple[list[SchemaT] | list[dict[str, Any]], int]": + """Execute a select statement and return both the data and total count. + + This is an alias for :meth:`select_with_total` provided for users familiar + with asyncpg's fetch() naming convention. + + This method is designed for pagination scenarios where you need both + the current page of data and the total number of rows that match the query. + + See Also: + select_with_total(): Primary method with identical behavior and full documentation + """ + return self.select_with_total( + statement, *parameters, schema_type=schema_type, statement_config=statement_config, **kwargs + ) + def _execute_stack_operation(self, operation: "StackOperation") -> "SQLResult | ArrowResult | None": kwargs = dict(operation.keyword_arguments) if operation.keyword_arguments else {} diff --git a/tests/unit/test_driver/test_fetch_aliases.py b/tests/unit/test_driver/test_fetch_aliases.py new file mode 100644 index 000000000..842b75ff3 --- /dev/null +++ b/tests/unit/test_driver/test_fetch_aliases.py @@ -0,0 +1,507 @@ +# pyright: reportPrivateUsage=false +"""Tests for fetch* method aliases in SyncDriverAdapterBase and AsyncDriverAdapterBase. + +Tests that all 14 fetch* methods (7 sync + 7 async) exist, have matching signatures, +and delegate correctly to their corresponding select* methods. + +Uses function-based pytest approach as per AGENTS.md requirements. +""" + +import inspect +from typing import Any +from unittest.mock import AsyncMock, Mock + +import pytest + +from sqlspec.driver._async import AsyncDriverAdapterBase +from sqlspec.driver._sync import SyncDriverAdapterBase + +pytestmark = pytest.mark.xdist_group("driver") + + +# Test method existence and signature equivalence + + +def test_sync_fetch_method_exists() -> None: + """Test that fetch() method exists on SyncDriverAdapterBase.""" + assert hasattr(SyncDriverAdapterBase, "fetch") + assert callable(getattr(SyncDriverAdapterBase, "fetch")) + + +def test_sync_fetch_one_method_exists() -> None: + """Test that fetch_one() method exists on SyncDriverAdapterBase.""" + assert hasattr(SyncDriverAdapterBase, "fetch_one") + assert callable(getattr(SyncDriverAdapterBase, "fetch_one")) + + +def test_sync_fetch_one_or_none_method_exists() -> None: + """Test that fetch_one_or_none() method exists on SyncDriverAdapterBase.""" + assert hasattr(SyncDriverAdapterBase, "fetch_one_or_none") + assert callable(getattr(SyncDriverAdapterBase, "fetch_one_or_none")) + + +def test_sync_fetch_value_method_exists() -> None: + """Test that fetch_value() method exists on SyncDriverAdapterBase.""" + assert hasattr(SyncDriverAdapterBase, "fetch_value") + assert callable(getattr(SyncDriverAdapterBase, "fetch_value")) + + +def test_sync_fetch_value_or_none_method_exists() -> None: + """Test that fetch_value_or_none() method exists on SyncDriverAdapterBase.""" + assert hasattr(SyncDriverAdapterBase, "fetch_value_or_none") + assert callable(getattr(SyncDriverAdapterBase, "fetch_value_or_none")) + + +def test_sync_fetch_to_arrow_method_exists() -> None: + """Test that fetch_to_arrow() method exists on SyncDriverAdapterBase.""" + assert hasattr(SyncDriverAdapterBase, "fetch_to_arrow") + assert callable(getattr(SyncDriverAdapterBase, "fetch_to_arrow")) + + +def test_sync_fetch_with_total_method_exists() -> None: + """Test that fetch_with_total() method exists on SyncDriverAdapterBase.""" + assert hasattr(SyncDriverAdapterBase, "fetch_with_total") + assert callable(getattr(SyncDriverAdapterBase, "fetch_with_total")) + + +def test_async_fetch_method_exists() -> None: + """Test that fetch() method exists on AsyncDriverAdapterBase.""" + assert hasattr(AsyncDriverAdapterBase, "fetch") + assert callable(getattr(AsyncDriverAdapterBase, "fetch")) + + +def test_async_fetch_one_method_exists() -> None: + """Test that fetch_one() method exists on AsyncDriverAdapterBase.""" + assert hasattr(AsyncDriverAdapterBase, "fetch_one") + assert callable(getattr(AsyncDriverAdapterBase, "fetch_one")) + + +def test_async_fetch_one_or_none_method_exists() -> None: + """Test that fetch_one_or_none() method exists on AsyncDriverAdapterBase.""" + assert hasattr(AsyncDriverAdapterBase, "fetch_one_or_none") + assert callable(getattr(AsyncDriverAdapterBase, "fetch_one_or_none")) + + +def test_async_fetch_value_method_exists() -> None: + """Test that fetch_value() method exists on AsyncDriverAdapterBase.""" + assert hasattr(AsyncDriverAdapterBase, "fetch_value") + assert callable(getattr(AsyncDriverAdapterBase, "fetch_value")) + + +def test_async_fetch_value_or_none_method_exists() -> None: + """Test that fetch_value_or_none() method exists on AsyncDriverAdapterBase.""" + assert hasattr(AsyncDriverAdapterBase, "fetch_value_or_none") + assert callable(getattr(AsyncDriverAdapterBase, "fetch_value_or_none")) + + +def test_async_fetch_to_arrow_method_exists() -> None: + """Test that fetch_to_arrow() method exists on AsyncDriverAdapterBase.""" + assert hasattr(AsyncDriverAdapterBase, "fetch_to_arrow") + assert callable(getattr(AsyncDriverAdapterBase, "fetch_to_arrow")) + + +def test_async_fetch_with_total_method_exists() -> None: + """Test that fetch_with_total() method exists on AsyncDriverAdapterBase.""" + assert hasattr(AsyncDriverAdapterBase, "fetch_with_total") + assert callable(getattr(AsyncDriverAdapterBase, "fetch_with_total")) + + +# Test signature equivalence between fetch* and select* methods + + +def test_sync_fetch_signature_matches_select() -> None: + """Test that fetch() signature matches select() signature.""" + fetch_sig = inspect.signature(SyncDriverAdapterBase.fetch) + select_sig = inspect.signature(SyncDriverAdapterBase.select) + + # Parameters should be identical + assert fetch_sig.parameters == select_sig.parameters + + +def test_sync_fetch_one_signature_matches_select_one() -> None: + """Test that fetch_one() signature matches select_one() signature.""" + fetch_sig = inspect.signature(SyncDriverAdapterBase.fetch_one) + select_sig = inspect.signature(SyncDriverAdapterBase.select_one) + + assert fetch_sig.parameters == select_sig.parameters + + +def test_sync_fetch_one_or_none_signature_matches_select_one_or_none() -> None: + """Test that fetch_one_or_none() signature matches select_one_or_none() signature.""" + fetch_sig = inspect.signature(SyncDriverAdapterBase.fetch_one_or_none) + select_sig = inspect.signature(SyncDriverAdapterBase.select_one_or_none) + + assert fetch_sig.parameters == select_sig.parameters + + +def test_sync_fetch_value_signature_matches_select_value() -> None: + """Test that fetch_value() signature matches select_value() signature.""" + fetch_sig = inspect.signature(SyncDriverAdapterBase.fetch_value) + select_sig = inspect.signature(SyncDriverAdapterBase.select_value) + + assert fetch_sig.parameters == select_sig.parameters + + +def test_sync_fetch_value_or_none_signature_matches_select_value_or_none() -> None: + """Test that fetch_value_or_none() signature matches select_value_or_none() signature.""" + fetch_sig = inspect.signature(SyncDriverAdapterBase.fetch_value_or_none) + select_sig = inspect.signature(SyncDriverAdapterBase.select_value_or_none) + + assert fetch_sig.parameters == select_sig.parameters + + +def test_sync_fetch_to_arrow_signature_matches_select_to_arrow() -> None: + """Test that fetch_to_arrow() signature matches select_to_arrow() signature.""" + fetch_sig = inspect.signature(SyncDriverAdapterBase.fetch_to_arrow) + select_sig = inspect.signature(SyncDriverAdapterBase.select_to_arrow) + + assert fetch_sig.parameters == select_sig.parameters + + +def test_sync_fetch_with_total_signature_matches_select_with_total() -> None: + """Test that fetch_with_total() signature matches select_with_total() signature.""" + fetch_sig = inspect.signature(SyncDriverAdapterBase.fetch_with_total) + select_sig = inspect.signature(SyncDriverAdapterBase.select_with_total) + + assert fetch_sig.parameters == select_sig.parameters + + +def test_async_fetch_signature_matches_select() -> None: + """Test that async fetch() signature matches async select() signature.""" + fetch_sig = inspect.signature(AsyncDriverAdapterBase.fetch) + select_sig = inspect.signature(AsyncDriverAdapterBase.select) + + assert fetch_sig.parameters == select_sig.parameters + + +def test_async_fetch_one_signature_matches_select_one() -> None: + """Test that async fetch_one() signature matches async select_one() signature.""" + fetch_sig = inspect.signature(AsyncDriverAdapterBase.fetch_one) + select_sig = inspect.signature(AsyncDriverAdapterBase.select_one) + + assert fetch_sig.parameters == select_sig.parameters + + +def test_async_fetch_one_or_none_signature_matches_select_one_or_none() -> None: + """Test that async fetch_one_or_none() signature matches async select_one_or_none() signature.""" + fetch_sig = inspect.signature(AsyncDriverAdapterBase.fetch_one_or_none) + select_sig = inspect.signature(AsyncDriverAdapterBase.select_one_or_none) + + assert fetch_sig.parameters == select_sig.parameters + + +def test_async_fetch_value_signature_matches_select_value() -> None: + """Test that async fetch_value() signature matches async select_value() signature.""" + fetch_sig = inspect.signature(AsyncDriverAdapterBase.fetch_value) + select_sig = inspect.signature(AsyncDriverAdapterBase.select_value) + + assert fetch_sig.parameters == select_sig.parameters + + +def test_async_fetch_value_or_none_signature_matches_select_value_or_none() -> None: + """Test that async fetch_value_or_none() signature matches async select_value_or_none() signature.""" + fetch_sig = inspect.signature(AsyncDriverAdapterBase.fetch_value_or_none) + select_sig = inspect.signature(AsyncDriverAdapterBase.select_value_or_none) + + assert fetch_sig.parameters == select_sig.parameters + + +def test_async_fetch_to_arrow_signature_matches_select_to_arrow() -> None: + """Test that async fetch_to_arrow() signature matches async select_to_arrow() signature.""" + fetch_sig = inspect.signature(AsyncDriverAdapterBase.fetch_to_arrow) + select_sig = inspect.signature(AsyncDriverAdapterBase.select_to_arrow) + + assert fetch_sig.parameters == select_sig.parameters + + +def test_async_fetch_with_total_signature_matches_select_with_total() -> None: + """Test that async fetch_with_total() signature matches async select_with_total() signature.""" + fetch_sig = inspect.signature(AsyncDriverAdapterBase.fetch_with_total) + select_sig = inspect.signature(AsyncDriverAdapterBase.select_with_total) + + assert fetch_sig.parameters == select_sig.parameters + + +# Test delegation behavior using mocks + + +def test_sync_fetch_delegates_to_select() -> None: + """Test that fetch() delegates to select() with identical arguments.""" + # Create mock driver with mocked select method + mock_driver = Mock(spec=SyncDriverAdapterBase) + mock_driver.select = Mock(return_value=[{"id": 1}]) + + # Call the real fetch method implementation + result = SyncDriverAdapterBase.fetch( + mock_driver, "SELECT * FROM users", {"id": 1}, schema_type=None, statement_config=None + ) + + # Verify select was called with same arguments + mock_driver.select.assert_called_once_with( + "SELECT * FROM users", {"id": 1}, schema_type=None, statement_config=None + ) + assert result == [{"id": 1}] + + +def test_sync_fetch_one_delegates_to_select_one() -> None: + """Test that fetch_one() delegates to select_one() with identical arguments.""" + mock_driver = Mock(spec=SyncDriverAdapterBase) + mock_driver.select_one = Mock(return_value={"id": 1}) + + result = SyncDriverAdapterBase.fetch_one( + mock_driver, "SELECT * FROM users WHERE id = ?", {"id": 1}, schema_type=None, statement_config=None + ) + + mock_driver.select_one.assert_called_once_with( + "SELECT * FROM users WHERE id = ?", {"id": 1}, schema_type=None, statement_config=None + ) + assert result == {"id": 1} + + +def test_sync_fetch_one_or_none_delegates_to_select_one_or_none() -> None: + """Test that fetch_one_or_none() delegates to select_one_or_none() with identical arguments.""" + mock_driver = Mock(spec=SyncDriverAdapterBase) + mock_driver.select_one_or_none = Mock(return_value=None) + + result = SyncDriverAdapterBase.fetch_one_or_none( + mock_driver, "SELECT * FROM users WHERE id = ?", {"id": 999}, schema_type=None, statement_config=None + ) + + mock_driver.select_one_or_none.assert_called_once_with( + "SELECT * FROM users WHERE id = ?", {"id": 999}, schema_type=None, statement_config=None + ) + assert result is None + + +def test_sync_fetch_value_delegates_to_select_value() -> None: + """Test that fetch_value() delegates to select_value() with identical arguments.""" + mock_driver = Mock(spec=SyncDriverAdapterBase) + mock_driver.select_value = Mock(return_value=42) + + result = SyncDriverAdapterBase.fetch_value(mock_driver, "SELECT COUNT(*) FROM users", statement_config=None) + + mock_driver.select_value.assert_called_once_with("SELECT COUNT(*) FROM users", statement_config=None) + assert result == 42 + + +def test_sync_fetch_value_or_none_delegates_to_select_value_or_none() -> None: + """Test that fetch_value_or_none() delegates to select_value_or_none() with identical arguments.""" + mock_driver = Mock(spec=SyncDriverAdapterBase) + mock_driver.select_value_or_none = Mock(return_value=None) + + result = SyncDriverAdapterBase.fetch_value_or_none( + mock_driver, "SELECT MAX(id) FROM empty_table", statement_config=None + ) + + mock_driver.select_value_or_none.assert_called_once_with("SELECT MAX(id) FROM empty_table", statement_config=None) + assert result is None + + +def test_sync_fetch_to_arrow_delegates_to_select_to_arrow() -> None: + """Test that fetch_to_arrow() delegates to select_to_arrow() with identical arguments.""" + mock_driver = Mock(spec=SyncDriverAdapterBase) + mock_arrow_result = Mock() + mock_driver.select_to_arrow = Mock(return_value=mock_arrow_result) + + result = SyncDriverAdapterBase.fetch_to_arrow( + mock_driver, + "SELECT * FROM users", + statement_config=None, + return_format="table", + native_only=False, + batch_size=None, + arrow_schema=None, + ) + + mock_driver.select_to_arrow.assert_called_once_with( + "SELECT * FROM users", + statement_config=None, + return_format="table", + native_only=False, + batch_size=None, + arrow_schema=None, + ) + assert result == mock_arrow_result + + +def test_sync_fetch_with_total_delegates_to_select_with_total() -> None: + """Test that fetch_with_total() delegates to select_with_total() with identical arguments.""" + mock_driver = Mock(spec=SyncDriverAdapterBase) + mock_driver.select_with_total = Mock(return_value=([{"id": 1}, {"id": 2}], 100)) + + result = SyncDriverAdapterBase.fetch_with_total( + mock_driver, "SELECT * FROM users LIMIT 2", schema_type=None, statement_config=None + ) + + mock_driver.select_with_total.assert_called_once_with( + "SELECT * FROM users LIMIT 2", schema_type=None, statement_config=None + ) + assert result == ([{"id": 1}, {"id": 2}], 100) + + +@pytest.mark.asyncio +async def test_async_fetch_delegates_to_select() -> None: + """Test that async fetch() delegates to async select() with identical arguments.""" + mock_driver = AsyncMock(spec=AsyncDriverAdapterBase) + mock_driver.select = AsyncMock(return_value=[{"id": 1}]) + + result = await AsyncDriverAdapterBase.fetch( + mock_driver, "SELECT * FROM users", {"id": 1}, schema_type=None, statement_config=None + ) + + mock_driver.select.assert_called_once_with( + "SELECT * FROM users", {"id": 1}, schema_type=None, statement_config=None + ) + assert result == [{"id": 1}] + + +@pytest.mark.asyncio +async def test_async_fetch_one_delegates_to_select_one() -> None: + """Test that async fetch_one() delegates to async select_one() with identical arguments.""" + mock_driver = AsyncMock(spec=AsyncDriverAdapterBase) + mock_driver.select_one = AsyncMock(return_value={"id": 1}) + + result = await AsyncDriverAdapterBase.fetch_one( + mock_driver, "SELECT * FROM users WHERE id = ?", {"id": 1}, schema_type=None, statement_config=None + ) + + mock_driver.select_one.assert_called_once_with( + "SELECT * FROM users WHERE id = ?", {"id": 1}, schema_type=None, statement_config=None + ) + assert result == {"id": 1} + + +@pytest.mark.asyncio +async def test_async_fetch_one_or_none_delegates_to_select_one_or_none() -> None: + """Test that async fetch_one_or_none() delegates to async select_one_or_none() with identical arguments.""" + mock_driver = AsyncMock(spec=AsyncDriverAdapterBase) + mock_driver.select_one_or_none = AsyncMock(return_value=None) + + result = await AsyncDriverAdapterBase.fetch_one_or_none( + mock_driver, "SELECT * FROM users WHERE id = ?", {"id": 999}, schema_type=None, statement_config=None + ) + + mock_driver.select_one_or_none.assert_called_once_with( + "SELECT * FROM users WHERE id = ?", {"id": 999}, schema_type=None, statement_config=None + ) + assert result is None + + +@pytest.mark.asyncio +async def test_async_fetch_value_delegates_to_select_value() -> None: + """Test that async fetch_value() delegates to async select_value() with identical arguments.""" + mock_driver = AsyncMock(spec=AsyncDriverAdapterBase) + mock_driver.select_value = AsyncMock(return_value=42) + + result = await AsyncDriverAdapterBase.fetch_value(mock_driver, "SELECT COUNT(*) FROM users", statement_config=None) + + mock_driver.select_value.assert_called_once_with("SELECT COUNT(*) FROM users", statement_config=None) + assert result == 42 + + +@pytest.mark.asyncio +async def test_async_fetch_value_or_none_delegates_to_select_value_or_none() -> None: + """Test that async fetch_value_or_none() delegates to async select_value_or_none() with identical arguments.""" + mock_driver = AsyncMock(spec=AsyncDriverAdapterBase) + mock_driver.select_value_or_none = AsyncMock(return_value=None) + + result = await AsyncDriverAdapterBase.fetch_value_or_none( + mock_driver, "SELECT MAX(id) FROM empty_table", statement_config=None + ) + + mock_driver.select_value_or_none.assert_called_once_with("SELECT MAX(id) FROM empty_table", statement_config=None) + assert result is None + + +@pytest.mark.asyncio +async def test_async_fetch_to_arrow_delegates_to_select_to_arrow() -> None: + """Test that async fetch_to_arrow() delegates to async select_to_arrow() with identical arguments.""" + mock_driver = AsyncMock(spec=AsyncDriverAdapterBase) + mock_arrow_result = Mock() + mock_driver.select_to_arrow = AsyncMock(return_value=mock_arrow_result) + + result = await AsyncDriverAdapterBase.fetch_to_arrow( + mock_driver, + "SELECT * FROM users", + statement_config=None, + return_format="table", + native_only=False, + batch_size=None, + arrow_schema=None, + ) + + mock_driver.select_to_arrow.assert_called_once_with( + "SELECT * FROM users", + statement_config=None, + return_format="table", + native_only=False, + batch_size=None, + arrow_schema=None, + ) + assert result == mock_arrow_result + + +@pytest.mark.asyncio +async def test_async_fetch_with_total_delegates_to_select_with_total() -> None: + """Test that async fetch_with_total() delegates to async select_with_total() with identical arguments.""" + mock_driver = AsyncMock(spec=AsyncDriverAdapterBase) + mock_driver.select_with_total = AsyncMock(return_value=([{"id": 1}, {"id": 2}], 100)) + + result = await AsyncDriverAdapterBase.fetch_with_total( + mock_driver, "SELECT * FROM users LIMIT 2", schema_type=None, statement_config=None + ) + + mock_driver.select_with_total.assert_called_once_with( + "SELECT * FROM users LIMIT 2", schema_type=None, statement_config=None + ) + assert result == ([{"id": 1}, {"id": 2}], 100) + + +# Test that fetch methods preserve schema_type argument handling + + +def test_sync_fetch_with_schema_type_argument() -> None: + """Test that fetch() correctly passes schema_type to select().""" + + class UserSchema: + """Sample schema class.""" + + def __init__(self, **kwargs: Any) -> None: + self.id = kwargs.get("id") + self.name = kwargs.get("name") + + mock_driver = Mock(spec=SyncDriverAdapterBase) + expected_result = [UserSchema(id=1, name="Alice")] + mock_driver.select = Mock(return_value=expected_result) + + result = SyncDriverAdapterBase.fetch( + mock_driver, "SELECT * FROM users", schema_type=UserSchema, statement_config=None + ) + + mock_driver.select.assert_called_once_with("SELECT * FROM users", schema_type=UserSchema, statement_config=None) + assert result == expected_result + + +@pytest.mark.asyncio +async def test_async_fetch_one_with_schema_type_argument() -> None: + """Test that async fetch_one() correctly passes schema_type to select_one().""" + + class UserSchema: + """Sample schema class.""" + + def __init__(self, **kwargs: Any) -> None: + self.id = kwargs.get("id") + self.name = kwargs.get("name") + + mock_driver = AsyncMock(spec=AsyncDriverAdapterBase) + expected_result = UserSchema(id=1, name="Alice") + mock_driver.select_one = AsyncMock(return_value=expected_result) + + result = await AsyncDriverAdapterBase.fetch_one( + mock_driver, "SELECT * FROM users WHERE id = 1", schema_type=UserSchema, statement_config=None + ) + + mock_driver.select_one.assert_called_once_with( + "SELECT * FROM users WHERE id = 1", schema_type=UserSchema, statement_config=None + ) + assert result == expected_result