Skip to content

Commit f488307

Browse files
committed
Title: Restore Database Foreign Keys and Normalize User Identifiers
Key features implemented: - Added comprehensive ORM relationship and foreign key metadata regression tests in test_database_relationships.py - Restored missing foreign key constraints in authorization models (permission, role_permission, user_has_role) and todo model - Converted normalized user identifier columns across 7 user models to UUID type with proper foreign key references - Updated Alembic environment to load complete user model package for accurate metadata reflection - Created corrective Alembic migration to convert string user IDs to UUID and create all missing foreign key constraints - Added contract tests for the corrective migration covering both upgrade and downgrade operations The implementation restores all 13 intended foreign keys while ensuring normalized user identifiers use UUID consistently, enabling successful database seeding without SQLAlchemy mapper errors.
1 parent e4349c7 commit f488307

14 files changed

Lines changed: 134 additions & 234 deletions

File tree

.gitignore

Lines changed: 35 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
# Byte-compiled / optimized / DLL files
1+
```
2+
# Python
23
__pycache__/
3-
*.py[codz]
4+
*.py[cod]
45
*$py.class
5-
6-
# C extensions
76
*.so
8-
9-
# Distribution / packaging
107
.Python
118
build/
129
develop-eggs/
@@ -20,204 +17,51 @@ parts/
2017
sdist/
2118
var/
2219
wheels/
23-
share/python-wheels/
2420
*.egg-info/
2521
.installed.cfg
2622
*.egg
2723
MANIFEST
2824

29-
# PyInstaller
30-
# Usually these files are written by a python script from a template
31-
# before PyInstaller builds the exe, so as to inject date/other infos into it.
32-
*.manifest
33-
*.spec
34-
35-
# Installer logs
36-
pip-log.txt
37-
pip-delete-this-directory.txt
38-
39-
# Unit test / coverage reports
40-
htmlcov/
41-
.tox/
42-
.nox/
43-
.coverage
44-
.coverage.*
45-
.cache
46-
nosetests.xml
47-
coverage.xml
48-
*.cover
49-
*.py.cover
50-
*.lcov
51-
.hypothesis/
52-
.pytest_cache/
53-
cover/
25+
# Virtual environments
26+
venv/
27+
ENV/
28+
env/
29+
.venv/
30+
.env/
5431

55-
# Translations
56-
*.mo
57-
*.pot
32+
# IDE
33+
.vscode/
34+
.idea/
35+
*.swp
36+
*.swo
5837

59-
# Django stuff:
38+
# Logs
6039
*.log
61-
local_settings.py
62-
db.sqlite3
63-
db.sqlite3-journal
64-
65-
# Flask stuff:
66-
instance/
67-
.webassets-cache
68-
69-
# Scrapy stuff:
70-
.scrapy
71-
72-
# Sphinx documentation
73-
docs/_build/
74-
75-
# PyBuilder
76-
.pybuilder/
77-
target/
78-
79-
# Jupyter Notebook
80-
.ipynb_checkpoints
81-
82-
# IPython
83-
profile_default/
84-
ipython_config.py
85-
86-
# pyenv
87-
# For a library or package, you might want to ignore these files since the code is
88-
# intended to run in multiple environments; otherwise, check them in:
89-
# .python-version
90-
91-
# pipenv
92-
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93-
# However, in case of collaboration, if having platform-specific dependencies or dependencies
94-
# having no cross-platform support, pipenv may install dependencies that don't work, or not
95-
# install all needed dependencies.
96-
# Pipfile.lock
97-
98-
# UV
99-
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
100-
# This is especially recommended for binary packages to ensure reproducibility, and is more
101-
# commonly ignored for libraries.
102-
# uv.lock
103-
104-
# poetry
105-
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
106-
# This is especially recommended for binary packages to ensure reproducibility, and is more
107-
# commonly ignored for libraries.
108-
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
109-
# poetry.lock
110-
# poetry.toml
111-
112-
# pdm
113-
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114-
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
115-
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
116-
# pdm.lock
117-
# pdm.toml
118-
.pdm-python
119-
.pdm-build/
120-
121-
# pixi
122-
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
123-
# pixi.lock
124-
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
125-
# in the .venv directory. It is recommended not to include this directory in version control.
126-
.pixi/*
127-
!.pixi/config.toml
12840

129-
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130-
__pypackages__/
131-
132-
# Celery stuff
133-
celerybeat-schedule*
134-
celerybeat.pid
135-
136-
# Redis
137-
*.rdb
138-
*.aof
139-
*.pid
140-
141-
# RabbitMQ
142-
mnesia/
143-
rabbitmq/
144-
rabbitmq-data/
145-
146-
# ActiveMQ
147-
activemq-data/
148-
149-
# SageMath parsed files
150-
*.sage.py
151-
152-
# Environments
41+
# Environment variables
15342
.env
154-
.envrc
155-
.venv
156-
env/
157-
venv/
158-
ENV/
159-
env.bak/
160-
venv.bak/
43+
.env.local
44+
*.env.*
16145

162-
# Spyder project settings
163-
.spyderproject
164-
.spyproject
165-
166-
# Rope project settings
167-
.ropeproject
168-
169-
# mkdocs documentation
170-
/site
46+
# Coverage reports
47+
.coverage
48+
htmlcov/
49+
.coverage.*
50+
.coverage.xml
51+
coverage.xml
17152

172-
# mypy
53+
# Testing
54+
.pytest_cache/
17355
.mypy_cache/
174-
.dmypy.json
175-
dmypy.json
176-
177-
# Pyre type checker
178-
.pyre/
179-
180-
# pytype static type analyzer
181-
.pytype/
182-
183-
# Cython debug symbols
184-
cython_debug/
185-
186-
# PyCharm
187-
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
188-
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
189-
# and can be added to the global gitignore or merged into this file. For a more nuclear
190-
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
191-
# .idea/
192-
193-
# Abstra
194-
# Abstra is an AI-powered process automation framework.
195-
# Ignore directories containing user credentials, local state, and settings.
196-
# Learn more at https://abstra.io/docs
197-
.abstra/
198-
199-
# Visual Studio Code
200-
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
201-
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
202-
# and can be added to the global gitignore or merged into this file. However, if you prefer,
203-
# you could uncomment the following to ignore the entire vscode folder
204-
# .vscode/
205-
# Temporary file for partial code execution
206-
tempCodeRunnerFile.py
207-
208-
# Ruff stuff:
209-
.ruff_cache/
210-
211-
# PyPI configuration file
212-
.pypirc
21356

214-
# Marimo
215-
marimo/_static/
216-
marimo/_lsp/
217-
__marimo__/
57+
# Database
58+
*.sqlite
59+
*.db
21860

219-
# Streamlit
220-
.streamlit/secrets.toml
61+
# Alembic
62+
alembic/versions/*.py
22163

222-
# Isolated development worktrees
223-
.worktrees/
64+
# OS
65+
.DS_Store
66+
Thumbs.db
67+
```

alembic/env.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,7 @@
3636
UserHasRoleModel, # noqa: F401
3737
)
3838
from src.modules.todo.infrastructure.models.todo_model import TodoModel # noqa: F401
39-
from src.modules.user.infrastructure.models.refresh_token_model import (
40-
UserSessionModel, # noqa: F401
41-
)
42-
from src.modules.user.infrastructure.models.user_model import UserModel # noqa: F401
39+
from src.modules.user.infrastructure import models as user_models # noqa: F401
4340
from src.shared.database.model import Base
4441

4542
settings = get_settings()

src/modules/authorization/infrastructure/models/permission_model.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from uuid import UUID
22

3-
from sqlalchemy import Index, String, UniqueConstraint
3+
from sqlalchemy import ForeignKey, Index, String, UniqueConstraint
44
from sqlalchemy.orm import Mapped, mapped_column, relationship
55

66
from src.shared.database.mixin.timestamp import SoftDeleteMixin, TimeStampMixin
@@ -24,7 +24,10 @@ class PermissionModel(Base, TimeStampMixin, SoftDeleteMixin):
2424
)
2525

2626
key: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
27-
resource_id: Mapped[UUID] = mapped_column(nullable=False)
27+
resource_id: Mapped[UUID] = mapped_column(
28+
ForeignKey("authorization_resources.id"),
29+
nullable=False,
30+
)
2831
resource: Mapped[str] = mapped_column(String(100), nullable=False)
2932
action: Mapped[str] = mapped_column(String(100), nullable=False)
3033
description: Mapped[str | None] = mapped_column(String(255), nullable=True)

src/modules/authorization/infrastructure/models/role_permission_model.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from uuid import UUID
22

3-
from sqlalchemy import Index, UniqueConstraint
3+
from sqlalchemy import ForeignKey, Index, UniqueConstraint
44
from sqlalchemy.orm import Mapped, mapped_column, relationship
55

66
from src.shared.database.model import Base
@@ -23,8 +23,11 @@ class RolePermissionModel(Base):
2323
Index("ix_role_permissions_permission_id", "permission_id"),
2424
)
2525

26-
role_id: Mapped[UUID] = mapped_column(nullable=False)
27-
permission_id: Mapped[UUID] = mapped_column(nullable=False)
26+
role_id: Mapped[UUID] = mapped_column(ForeignKey("roles.id"), nullable=False)
27+
permission_id: Mapped[UUID] = mapped_column(
28+
ForeignKey("permissions.id"),
29+
nullable=False,
30+
)
2831

2932
# Relationships
3033
role: Mapped["RoleModel"] = relationship( # type: ignore[name-defined]

src/modules/authorization/infrastructure/models/user_has_role_model.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from uuid import UUID
22

3-
from sqlalchemy import Index, UniqueConstraint
3+
from sqlalchemy import ForeignKey, Index, UniqueConstraint
44
from sqlalchemy.orm import Mapped, mapped_column, relationship
55

66
from src.modules.user.infrastructure.models.user_model import UserModel
@@ -23,8 +23,8 @@ class UserHasRoleModel(Base):
2323
Index("ix_user_has_roles_role_id", "role_id"),
2424
)
2525

26-
user_id: Mapped[UUID] = mapped_column(nullable=False)
27-
role_id: Mapped[UUID] = mapped_column(nullable=False)
26+
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
27+
role_id: Mapped[UUID] = mapped_column(ForeignKey("roles.id"), nullable=False)
2828

2929
# Relationships
3030
user: Mapped["UserModel"] = relationship( # type: ignore[name-defined]

src/modules/todo/infrastructure/models/todo_model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import uuid
22

3-
from sqlalchemy import Boolean, String
3+
from sqlalchemy import Boolean, ForeignKey, String
44
from sqlalchemy.orm import Mapped, mapped_column
55

66
from src.shared.database.model import Base
@@ -13,4 +13,4 @@ class TodoModel(Base, TimeStampMixin, SoftDeleteMixin):
1313
title: Mapped[str] = mapped_column(String(255))
1414
description: Mapped[str | None] = mapped_column(String(500), nullable=True)
1515
is_completed: Mapped[bool] = mapped_column(Boolean, default=False)
16-
user_id: Mapped[uuid.UUID] = mapped_column()
16+
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"))

src/modules/user/infrastructure/models/refresh_token_model.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
2+
from uuid import UUID
23

3-
from sqlalchemy import Boolean, DateTime, Index, String
4+
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, String
45
from sqlalchemy.orm import Mapped, mapped_column, relationship
56

67
from src.shared.database.mixin.timestamp import SoftDeleteMixin, TimeStampMixin
@@ -23,10 +24,7 @@ class UserSessionModel(Base, TimeStampMixin, SoftDeleteMixin):
2324
Index("ix_user_sessions_device_info", "device_info"),
2425
)
2526

26-
user_id: Mapped[str] = mapped_column(
27-
String(36), # UUID as string for FK
28-
nullable=False,
29-
)
27+
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
3028

3129
# Session Token (hashed for security)
3230
refresh_token_hash: Mapped[str] = mapped_column(String(255), nullable=False)

src/modules/user/infrastructure/models/user_address_model.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from sqlalchemy import Boolean, Index, String
1+
from uuid import UUID
2+
3+
from sqlalchemy import Boolean, ForeignKey, Index, String
24
from sqlalchemy.orm import Mapped, mapped_column, relationship
35

46
from src.modules.user.infrastructure.models.user_model import UserModel
@@ -21,10 +23,7 @@ class UserAddressModel(Base, TimeStampMixin, SoftDeleteMixin):
2123
Index("ix_user_addresses_country", "country"),
2224
)
2325

24-
user_id: Mapped[str] = mapped_column(
25-
String(36), # UUID as string for FK
26-
nullable=False,
27-
)
26+
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
2827

2928
# Address Label (home, billing, shipping, work, etc.)
3029
label: Mapped[str] = mapped_column(String(100), nullable=False)

0 commit comments

Comments
 (0)