diff --git a/app/database.py b/app/database.py index 0127fb0..0eec57d 100644 --- a/app/database.py +++ b/app/database.py @@ -19,7 +19,7 @@ from collections.abc import AsyncGenerator from pathlib import PurePath from typing import Final -from sqlalchemy import BigInteger, ForeignKey, String, Text +from sqlalchemy import BigInteger, ForeignKey, MetaData, String, Text, UniqueConstraint from sqlalchemy.ext.asyncio import ( AsyncAttrs, AsyncEngine, @@ -27,7 +27,12 @@ from sqlalchemy.ext.asyncio import ( async_sessionmaker, create_async_engine, ) -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship +from sqlalchemy.orm import ( + Mapped, + declarative_base, + mapped_column, + relationship, +) DB_PATH: Final[PurePath] = PurePath(__file__).parent.parent / "db.sqlite3" @@ -39,6 +44,16 @@ async_session_maker: Final[async_sessionmaker[AsyncSession]] = async_sessionmake engine, expire_on_commit=False, autoflush=False, autocommit=False ) +metadata = MetaData( + naming_convention={ + "ix": "ix_%(table_name)s_%(column_0_N_name)s ", + "uq": "uq_%(table_name)s_%(column_0_N_name)s ", + "ck": "ck_%(table_name)s_%(constraint_name)s ", + "fk": "fk_%(table_name)s_%(column_0_N_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s", + } +) + async def get_async_session() -> AsyncGenerator[AsyncSession, None]: """Get an async session.""" @@ -46,10 +61,7 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]: yield session -class Base(AsyncAttrs, DeclarativeBase): - """Declarative base for database models.""" - - pass +Base = declarative_base(metadata=metadata, cls=AsyncAttrs) class Repo(Base): @@ -66,6 +78,7 @@ class Repo(Base): dependencies: Mapped[list["Dependency"]] = relationship( "Dependency", secondary="repo_dependency", back_populates="repos" ) + __table_args__ = (UniqueConstraint("url", "source_graph_repo_id"),) class Dependency(Base): diff --git a/app/source_graph/mapper.py b/app/source_graph/mapper.py index b69f36e..857fe11 100644 --- a/app/source_graph/mapper.py +++ b/app/source_graph/mapper.py @@ -21,7 +21,7 @@ async def create_or_update_repos_from_source_graph_repos_data( """ insert_statement = sqlalchemy.dialects.sqlite.insert(database.Repo) update_statement = insert_statement.on_conflict_do_update( - index_elements=[database.Repo.url], + index_elements=[database.Repo.url, database.Repo.source_graph_repo_id], set_={ "url": insert_statement.excluded.url, "description": insert_statement.excluded.description, diff --git a/db.sqlite3 b/db.sqlite3 index 9034064..cfe675e 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/migrations/versions/0232d84a5aea_create_repo_dependency_and_.py b/migrations/versions/90eb9d1f9267_set_up_the_database.py similarity index 52% rename from migrations/versions/0232d84a5aea_create_repo_dependency_and_.py rename to migrations/versions/90eb9d1f9267_set_up_the_database.py index 8e8a4c1..03efee8 100644 --- a/migrations/versions/0232d84a5aea_create_repo_dependency_and_.py +++ b/migrations/versions/90eb9d1f9267_set_up_the_database.py @@ -1,15 +1,15 @@ -"""Create Repo, Dependency and RepoDependency tables +"""Set up the database -Revision ID: 0232d84a5aea +Revision ID: 90eb9d1f9267 Revises: -Create Date: 2023-08-02 22:14:12.910175 +Create Date: 2023-08-15 14:13:30.562069 """ import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision = "0232d84a5aea" +revision = "90eb9d1f9267" down_revision = None branch_labels = None depends_on = None @@ -21,8 +21,8 @@ def upgrade() -> None: "dependency", sa.Column("id", sa.Integer(), nullable=False), sa.Column("name", sa.String(length=255), nullable=False), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("name"), + sa.PrimaryKeyConstraint("id", name=op.f("pk_dependency")), + sa.UniqueConstraint("name", name=op.f("uq_dependency_name ")), ) op.create_table( "repo", @@ -31,19 +31,36 @@ def upgrade() -> None: sa.Column("description", sa.Text(), nullable=False), sa.Column("stars", sa.BigInteger(), nullable=False), sa.Column("source_graph_repo_id", sa.BigInteger(), nullable=True), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("source_graph_repo_id"), - sa.UniqueConstraint("url"), + sa.PrimaryKeyConstraint("id", name=op.f("pk_repo")), + sa.UniqueConstraint( + "source_graph_repo_id", name=op.f("uq_repo_source_graph_repo_id ") + ), + sa.UniqueConstraint( + "url", + "source_graph_repo_id", + name=op.f("uq_repo_url_source_graph_repo_id "), + ), + sa.UniqueConstraint("url", name=op.f("uq_repo_url ")), ) op.create_table( "repo_dependency", sa.Column("repo_id", sa.Integer(), nullable=False), sa.Column("dependency_id", sa.Integer(), nullable=False), sa.ForeignKeyConstraint( - ["dependency_id"], ["dependency.id"], ondelete="CASCADE" + ["dependency_id"], + ["dependency.id"], + name=op.f("fk_repo_dependency_dependency_id_dependency"), + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["repo_id"], + ["repo.id"], + name=op.f("fk_repo_dependency_repo_id_repo"), + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint( + "repo_id", "dependency_id", name=op.f("pk_repo_dependency") ), - sa.ForeignKeyConstraint(["repo_id"], ["repo.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("repo_id", "dependency_id"), ) # ### end Alembic commands ### diff --git a/pyproject.toml b/pyproject.toml index 267295d..cc65b0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "alembic", "fastapi[all]", "httpx-sse", - "sqlalchemy[asyncio]", + "sqlalchemy[asyncio,mypy]", "stamina", "third-party-imports", "typer[all]", @@ -114,7 +114,8 @@ convention = "numpy" [tool.mypy] plugins = [ - "pydantic.mypy" + "pydantic.mypy", + "sqlalchemy.ext.mypy.plugin", ] strict = true exclude = [ diff --git a/requirements/base.txt b/requirements/base.txt index 7487eb5..d4d113b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -66,6 +66,10 @@ markupsafe==2.1.3 # mako mdurl==0.1.2 # via markdown-it-py +mypy==1.4.1 + # via sqlalchemy +mypy-extensions==1.0.0 + # via mypy orjson==3.9.2 # via fastapi pydantic==2.1.1 @@ -100,7 +104,7 @@ sniffio==1.3.0 # anyio # httpcore # httpx -sqlalchemy[asyncio]==2.0.19 +sqlalchemy[asyncio,mypy]==2.0.19 # via # alembic # awesome-fastapi-projects (pyproject.toml) @@ -118,6 +122,7 @@ typing-extensions==4.7.1 # via # alembic # fastapi + # mypy # pydantic # pydantic-core # sqlalchemy diff --git a/requirements/dev.txt b/requirements/dev.txt index d94e65a..67febb3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -95,7 +95,9 @@ matplotlib-inline==0.1.6 mdurl==0.1.2 # via markdown-it-py mypy==1.4.1 - # via awesome-fastapi-projects (pyproject.toml) + # via + # awesome-fastapi-projects (pyproject.toml) + # sqlalchemy mypy-extensions==1.0.0 # via # black @@ -176,7 +178,7 @@ sniffio==1.3.0 # anyio # httpcore # httpx -sqlalchemy[asyncio]==2.0.19 +sqlalchemy[asyncio,mypy]==2.0.19 # via # alembic # awesome-fastapi-projects (pyproject.toml) diff --git a/requirements/test.txt b/requirements/test.txt index 4d1ce82..39dbb0f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -75,6 +75,10 @@ markupsafe==2.1.3 # mako mdurl==0.1.2 # via markdown-it-py +mypy==1.4.1 + # via sqlalchemy +mypy-extensions==1.0.0 + # via mypy orjson==3.9.2 # via fastapi packaging==23.1 @@ -133,7 +137,7 @@ sniffio==1.3.0 # anyio # httpcore # httpx -sqlalchemy[asyncio]==2.0.19 +sqlalchemy[asyncio,mypy]==2.0.19 # via # alembic # awesome-fastapi-projects (pyproject.toml) @@ -151,6 +155,7 @@ typing-extensions==4.7.1 # via # alembic # fastapi + # mypy # polyfactory # pydantic # pydantic-core