diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/config/config.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/config/config.py index 224b70ff..e196a942 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/config/config.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/config/config.py @@ -6,7 +6,7 @@ @dataclass class SQLiteConfig: driver: str = "sqlite" - url: Optional[str] = env("DB_URL", None) + url: Optional[str] = env("DB_URL", None, cast=False) database: str = env("DB_DATABASE", "database.sqlite") options: Optional[Dict[str, Any]] = None @@ -14,13 +14,13 @@ class SQLiteConfig: @dataclass class MySQLConfig: driver: str = "mysql" - url: Optional[str] = env("DB_URL", None) + url: Optional[str] = env("DB_URL", None, cast=False) host: str = env("DB_HOST", "127.0.0.1") port: int = env("DB_PORT", 3306) database: str = env("DB_DATABASE", "inertia") username: str = env("DB_USERNAME", "root") - password: str = env("DB_PASSWORD", "") - unix_socket: str = env("DB_SOCKET", "") + password: str = env("DB_PASSWORD", "", cast=False) + unix_socket: str = env("DB_SOCKET", "", cast=False) charset: str = env("DB_CHARSET", "utf8mb4") collation: str = env("DB_COLLATION", "utf8mb4_unicode_ci") options: Optional[Dict[str, Any]] = None @@ -29,12 +29,12 @@ class MySQLConfig: @dataclass class PostgresConfig: driver: str = "postgres" - url: Optional[str] = env("DB_URL", None) + url: Optional[str] = env("DB_URL", None, cast=False) host: str = env("DB_HOST", "127.0.0.1") port: int = env("DB_PORT", 5432) database: str = env("DB_DATABASE", "inertia") username: str = env("DB_USERNAME", "postgres") - password: str = env("DB_PASSWORD", "") + password: str = env("DB_PASSWORD", "", cast=False) charset: str = env("DB_CHARSET", "utf8") sslmode: str = env("DB_SSLMODE", "prefer") options: Optional[Dict[str, Any]] = None diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/config/database.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/config/database.py index c175aa43..ff61fe9a 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/config/database.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/config/database.py @@ -1,15 +1,15 @@ from dataclasses import dataclass, field -from typing import Dict, Any +from typing import Any from fastapi_startkit.environment import env -from fastapi_startkit.masoniteorm import SQLiteConfig +from fastapi_startkit.masoniteorm.config.config import MySQLConfig, PostgresConfig, SQLiteConfig @dataclass class DatabaseConfig: default: str = field(default_factory=lambda: env("DB_CONNECTION", "pgsql")) - connections: Dict[str, Dict[str, Any]] = field( + connections: dict[str, SQLiteConfig | MySQLConfig | PostgresConfig | dict[str, Any]] = field( default_factory=lambda: { "sqlite": SQLiteConfig( driver="sqlite", @@ -19,6 +19,6 @@ class DatabaseConfig: } ) - migrations: Dict[str, Dict[str, Any]] = field( + migrations: dict[str, str] = field( default_factory=lambda: {"table": "migrations", "directory": "databases/migrations"} ) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/connection.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/connection.py index 54f5a198..f67faaf0 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/connection.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/connection.py @@ -12,6 +12,8 @@ if TYPE_CHECKING: from typing import Self + from fastapi_startkit.masoniteorm.query.grammars.BaseGrammar import BaseGrammar + class Transaction: def __init__(self, owner: Connection): @@ -100,11 +102,13 @@ def query(self) -> QueryBuilder: async def get_connection(self) -> AsyncConnection: return self.connection or await self.engine.connect() - def get_query_grammar(cls): - pass + @classmethod + def get_query_grammar(cls) -> type[BaseGrammar] | None: + return None - def get_post_processor(self): - pass + @classmethod + def get_post_processor(cls) -> type | None: + return None async def begin_transaction(self) -> None: connection = self.connection @@ -199,7 +203,7 @@ async def delete(self, query: str, bindings: list | None = None) -> int: async def select(self, query: str, bindings: list | None = None) -> list[dict]: result = await self.run(query, bindings) - return result.mappings().all() + return [dict(row) for row in result.mappings().all()] async def select_one(self, query: str, bindings: list | None = None) -> dict | None: result = await self.run(query, bindings) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/factory.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/factory.py index e193b8c7..b038475e 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/factory.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/factory.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import TYPE_CHECKING, Any, cast from sqlalchemy import StaticPool from sqlalchemy.pool import NullPool @@ -12,6 +12,9 @@ ) from fastapi_startkit.masoniteorm.connections.mysql_connection import MySQLConnection +if TYPE_CHECKING: + from fastapi_startkit.application import Application + class ConnectionFactory: DRIVER_URLS = { @@ -64,7 +67,7 @@ def create_engine(cls, cfg: dict) -> AsyncEngine: kwargs: dict[str, Any] = {"echo": True} from fastapi_startkit.application import app - if app().is_testing(): + if cast("Application", app()).is_testing(): kwargs["poolclass"] = NullPool elif cfg["driver"] == "sqlite": kwargs["connect_args"] = {"check_same_thread": False} diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/mysql_connection.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/mysql_connection.py index a12ab4c5..71e4448d 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/mysql_connection.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/mysql_connection.py @@ -8,7 +8,7 @@ class MySQLConnection(Connection): """Async MySQL connection backed by aiomysql via SQLAlchemy.""" @classmethod - def get_query_grammar(cls): + def get_query_grammar(cls) -> type[MySQLGrammar]: return MySQLGrammar @classmethod @@ -16,5 +16,5 @@ def get_default_platform(cls): return MySQLPlatform @classmethod - def get_post_processor(cls): + def get_post_processor(cls) -> type[MySQLPostProcessor]: return MySQLPostProcessor diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/postgres_connection.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/postgres_connection.py index 43fb32b1..06cfaf3e 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/postgres_connection.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/postgres_connection.py @@ -13,7 +13,7 @@ async def insert_get_id(self, query: str, bindings: list | None = None) -> int | return row[0] if row is not None else None @classmethod - def get_query_grammar(cls): + def get_query_grammar(cls) -> type[PostgresGrammar]: return PostgresGrammar @classmethod @@ -21,5 +21,5 @@ def get_default_platform(cls): return PostgresPlatform @classmethod - def get_post_processor(cls): + def get_post_processor(cls) -> type[PostgresPostProcessor]: return PostgresPostProcessor diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/sqlite_connection.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/sqlite_connection.py index 3f705748..c746cb1a 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/sqlite_connection.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/sqlite_connection.py @@ -6,7 +6,7 @@ class SQliteConnection(Connection): @classmethod - def get_query_grammar(cls): + def get_query_grammar(cls) -> type[SQLiteGrammar]: return SQLiteGrammar @classmethod @@ -14,5 +14,5 @@ def get_default_platform(cls): return SQLitePlatform @classmethod - def get_post_processor(cls): + def get_post_processor(cls) -> type[SQLitePostProcessor]: return SQLitePostProcessor diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/testing/transaction.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/testing/transaction.py index 5c8cb03e..514195c0 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/testing/transaction.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/testing/transaction.py @@ -1,3 +1,9 @@ +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from fastapi_startkit.application import Application + + class DatabaseTransaction: async def asyncStartTestRun(self): from fastapi_startkit.masoniteorm.models import Model @@ -24,7 +30,7 @@ async def migrate_database(): from fastapi_startkit.masoniteorm.migrations import Migrator from fastapi_startkit.application import app as get_app - migration_dir = get_app().use_base_path("databases/migrations") + migration_dir = str(cast("Application", get_app()).use_base_path("databases/migrations")) migrator = Migrator(migration_directory=migration_dir) await migrator.fresh(ignore_fk=True) RefreshDatabase.migrated = True diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/test_connection_select.py b/fastapi_startkit/tests/masoniteorm/sqlite/test_connection_select.py new file mode 100644 index 00000000..572c6e4c --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/sqlite/test_connection_select.py @@ -0,0 +1,21 @@ +from fastapi_startkit.masoniteorm.connections.connection import Connection +from fastapi_startkit.masoniteorm.models import Model + +from ..fixtures.model import User +from .test_case import TestCase + + +class TestConnectionSelect(TestCase): + async def test_select_returns_plain_dicts(self): + await User.create({"email": "select@example.com", "name": "Select", "is_admin": False}) + + rows = await Model.db_manager.connection(None).select( + "SELECT email, name FROM users WHERE email = ?", ["select@example.com"] + ) + + assert rows == [{"email": "select@example.com", "name": "Select"}] + assert all(type(row) is dict for row in rows) + + def test_base_connection_has_no_grammar_or_processor(self): + assert Connection.get_query_grammar() is None + assert Connection.get_post_processor() is None diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/test_testing_transaction.py b/fastapi_startkit/tests/masoniteorm/sqlite/test_testing_transaction.py index ed0c625d..7f066244 100644 --- a/fastapi_startkit/tests/masoniteorm/sqlite/test_testing_transaction.py +++ b/fastapi_startkit/tests/masoniteorm/sqlite/test_testing_transaction.py @@ -1,4 +1,7 @@ -from fastapi_startkit.masoniteorm.testing.transaction import DatabaseTransaction +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch + +from fastapi_startkit.masoniteorm.testing.transaction import DatabaseTransaction, RefreshDatabase from ..fixtures.model import User from .test_case import TestCase @@ -14,3 +17,25 @@ async def test_start_and_stop_roll_back_writes(self): finally: await harness.asyncStopTestRun() assert await User.where("email", "harness@example.com").first() is None + + +class TestRefreshDatabaseMigrate(TestCase): + async def test_migrate_database_runs_fresh_once_with_string_directory(self): + application = MagicMock() + application.use_base_path.return_value = Path("/project/databases/migrations") + migrator = MagicMock() + migrator.return_value.fresh = AsyncMock() + + with ( + patch.object(RefreshDatabase, "migrated", False), + patch("fastapi_startkit.application.app", return_value=application), + patch("fastapi_startkit.masoniteorm.migrations.Migrator", migrator), + ): + await RefreshDatabase.migrate_database() + await RefreshDatabase.migrate_database() + + assert RefreshDatabase.migrated is True + + application.use_base_path.assert_called_once_with("databases/migrations") + migrator.assert_called_once_with(migration_directory="/project/databases/migrations") + migrator.return_value.fresh.assert_awaited_once_with(ignore_fk=True)