fix: add password_hash column migration and unit tests
CI Pipeline / backend-test (push) Successful in 4m5s
CI Pipeline / frontend-test (push) Successful in 27s
CI Pipeline / frontend-build (push) Successful in 30s
CI Pipeline / backend-lint (push) Successful in 11s
CI Pipeline / backend-test (pull_request) Successful in 4m11s
CI Pipeline / frontend-test (pull_request) Successful in 26s
CI Pipeline / frontend-build (pull_request) Successful in 30s
CI Pipeline / backend-lint (pull_request) Successful in 8s
CI Pipeline / quality-gate (push) Successful in 2s
CI Pipeline / quality-gate (pull_request) Successful in 2s
CI Pipeline / backend-test (push) Successful in 4m5s
CI Pipeline / frontend-test (push) Successful in 27s
CI Pipeline / frontend-build (push) Successful in 30s
CI Pipeline / backend-lint (push) Successful in 11s
CI Pipeline / backend-test (pull_request) Successful in 4m11s
CI Pipeline / frontend-test (pull_request) Successful in 26s
CI Pipeline / frontend-build (pull_request) Successful in 30s
CI Pipeline / backend-lint (pull_request) Successful in 8s
CI Pipeline / quality-gate (push) Successful in 2s
CI Pipeline / quality-gate (pull_request) Successful in 2s
- Add users.password_hash to _migrate_add_missing_columns pending list - Skip tables that don't exist yet (prevents crash on fresh DB) - Handle nullable columns (no DEFAULT clause) in ALTER TABLE - Fix SQLite column type (use sql_type instead of hardcoded TEXT) - Update migrate_roles.py to create schema columns/tables before seeding roles - Add unit tests for migration: adds column, nullable, idempotent, no-op
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""
|
||||
Migration seed: create the 'admin' role and assign it to existing is_admin=True users.
|
||||
Migration seed: add missing schema columns/tables, then create the 'admin' role
|
||||
and assign it to existing is_admin=True users.
|
||||
|
||||
Run once after deploying the updated schema. Idempotent — safe to run multiple times.
|
||||
|
||||
@@ -15,9 +16,84 @@ from app.database import async_session
|
||||
from app.user_models import User, Role, user_roles
|
||||
|
||||
|
||||
async def ensure_schema(session) -> None:
|
||||
"""Add missing columns/tables so the User model matches the live database.
|
||||
|
||||
- Adds ``password_hash`` to ``users`` (nullable — existing rows stay NULL).
|
||||
- Creates ``roles`` and ``user_roles`` tables if they do not exist.
|
||||
|
||||
All steps use ``IF NOT EXISTS`` / ``NOT EXISTS`` guards so the function is
|
||||
fully idempotent.
|
||||
"""
|
||||
conn = await session.connection()
|
||||
|
||||
# 1) Add password_hash column if missing
|
||||
result = await conn.execute(
|
||||
text(
|
||||
"SELECT 1 FROM information_schema.columns "
|
||||
"WHERE table_name = 'users' AND column_name = 'password_hash'"
|
||||
)
|
||||
)
|
||||
if not result.scalar():
|
||||
await conn.execute(
|
||||
text("ALTER TABLE users ADD COLUMN password_hash VARCHAR(255)")
|
||||
)
|
||||
print(" ✓ Added 'password_hash' column to 'users'")
|
||||
else:
|
||||
print(" ✓ 'users.password_hash' already exists")
|
||||
|
||||
# 2) Create 'roles' table if missing
|
||||
result = await conn.execute(
|
||||
text(
|
||||
"SELECT 1 FROM information_schema.tables "
|
||||
"WHERE table_name = 'roles'"
|
||||
)
|
||||
)
|
||||
if not result.scalar():
|
||||
await conn.execute(
|
||||
text(
|
||||
"CREATE TABLE IF NOT EXISTS roles ("
|
||||
"id SERIAL PRIMARY KEY, "
|
||||
"name VARCHAR(50) UNIQUE NOT NULL, "
|
||||
"description TEXT"
|
||||
")"
|
||||
)
|
||||
)
|
||||
print(" ✓ Created 'roles' table")
|
||||
else:
|
||||
print(" ✓ 'roles' table already exists")
|
||||
|
||||
# 3) Create 'user_roles' table if missing
|
||||
result = await conn.execute(
|
||||
text(
|
||||
"SELECT 1 FROM information_schema.tables "
|
||||
"WHERE table_name = 'user_roles'"
|
||||
)
|
||||
)
|
||||
if not result.scalar():
|
||||
await conn.execute(
|
||||
text(
|
||||
"CREATE TABLE IF NOT EXISTS user_roles ("
|
||||
"user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, "
|
||||
"role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE, "
|
||||
"PRIMARY KEY (user_id, role_id)"
|
||||
")"
|
||||
)
|
||||
)
|
||||
print(" ✓ Created 'user_roles' table")
|
||||
else:
|
||||
print(" ✓ 'user_roles' table already exists")
|
||||
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def migrate() -> None:
|
||||
"""Ensure 'admin' role exists and assign to existing is_admin=True users."""
|
||||
"""Ensure schema is up to date, 'admin' role exists, and legacy admins get the role."""
|
||||
async with async_session() as session:
|
||||
print("Ensuring schema is up to date...")
|
||||
await ensure_schema(session)
|
||||
|
||||
print("Running role migration...")
|
||||
# 1) Create 'admin' role if it doesn't exist
|
||||
result = await session.execute(select(Role).where(Role.name == "admin"))
|
||||
admin_role = result.scalar_one_or_none()
|
||||
|
||||
Reference in New Issue
Block a user