Fix SQLite migration issues by removing non-constant defaults

Changes:
- Removed server_default with CURRENT_TIMESTAMP from User model (SQLite doesn't support non-constant defaults in ALTER TABLE)
- Updated migrations 002 and 003 to add datetime columns without server defaults
- Added manual timestamp updates using SQLite's datetime('now') function in migrations
- Modified User model to handle timestamps in Python code instead of database defaults
- Updated profile routes to manually set updated_at timestamps on profile changes
- Enhanced User model __init__ to set default timestamps for new users

This resolves the 'Cannot add a column with non-constant default' SQLite error
while maintaining proper timestamp functionality.
This commit is contained in:
Automated Action 2025-06-24 19:53:37 +00:00
parent 025900c849
commit 3e90deba20
4 changed files with 30 additions and 8 deletions

View File

@ -27,7 +27,7 @@ def column_exists(table_name, column_name):
def upgrade() -> None: def upgrade() -> None:
# List of columns to add # List of columns to add (without server defaults for SQLite compatibility)
columns_to_add = [ columns_to_add = [
('first_name', sa.Column('first_name', sa.String(), nullable=True)), ('first_name', sa.Column('first_name', sa.String(), nullable=True)),
('last_name', sa.Column('last_name', sa.String(), nullable=True)), ('last_name', sa.Column('last_name', sa.String(), nullable=True)),
@ -35,7 +35,7 @@ def upgrade() -> None:
('bio', sa.Column('bio', sa.String(), nullable=True)), ('bio', sa.Column('bio', sa.String(), nullable=True)),
('preferred_language', sa.Column('preferred_language', sa.String(), nullable=True)), ('preferred_language', sa.Column('preferred_language', sa.String(), nullable=True)),
('timezone', sa.Column('timezone', sa.String(), nullable=True)), ('timezone', sa.Column('timezone', sa.String(), nullable=True)),
('updated_at', sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True)) ('updated_at', sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True)) # No server_default for SQLite
] ]
# Add columns only if they don't exist # Add columns only if they don't exist
@ -48,6 +48,9 @@ def upgrade() -> None:
op.execute("UPDATE users SET preferred_language = 'en' WHERE preferred_language IS NULL") op.execute("UPDATE users SET preferred_language = 'en' WHERE preferred_language IS NULL")
if column_exists('users', 'timezone'): if column_exists('users', 'timezone'):
op.execute("UPDATE users SET timezone = 'UTC' WHERE timezone IS NULL") op.execute("UPDATE users SET timezone = 'UTC' WHERE timezone IS NULL")
if column_exists('users', 'updated_at'):
# Set updated_at to current timestamp for existing users
op.execute("UPDATE users SET updated_at = datetime('now') WHERE updated_at IS NULL")
def downgrade() -> None: def downgrade() -> None:

View File

@ -53,12 +53,16 @@ def upgrade() -> None:
sa.Column('bio', sa.String(), nullable=True), sa.Column('bio', sa.String(), nullable=True),
sa.Column('preferred_language', sa.String(), nullable=True), sa.Column('preferred_language', sa.String(), nullable=True),
sa.Column('timezone', sa.String(), nullable=True), sa.Column('timezone', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), # Remove server_default for SQLite
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), # Remove server_default for SQLite
sa.PrimaryKeyConstraint('id') sa.PrimaryKeyConstraint('id')
) )
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
# Set default timestamps for new table
op.execute("UPDATE users SET created_at = datetime('now') WHERE created_at IS NULL")
op.execute("UPDATE users SET updated_at = datetime('now') WHERE updated_at IS NULL")
else: else:
# Table exists, check for missing profile columns and add them # Table exists, check for missing profile columns and add them
profile_columns = [ profile_columns = [
@ -68,7 +72,7 @@ def upgrade() -> None:
('bio', sa.Column('bio', sa.String(), nullable=True)), ('bio', sa.Column('bio', sa.String(), nullable=True)),
('preferred_language', sa.Column('preferred_language', sa.String(), nullable=True)), ('preferred_language', sa.Column('preferred_language', sa.String(), nullable=True)),
('timezone', sa.Column('timezone', sa.String(), nullable=True)), ('timezone', sa.Column('timezone', sa.String(), nullable=True)),
('updated_at', sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True)) ('updated_at', sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True)) # Remove server_default for SQLite
] ]
for column_name, column_def in profile_columns: for column_name, column_def in profile_columns:
@ -80,6 +84,8 @@ def upgrade() -> None:
op.execute("UPDATE users SET preferred_language = 'en' WHERE preferred_language IS NULL") op.execute("UPDATE users SET preferred_language = 'en' WHERE preferred_language IS NULL")
if column_exists('users', 'timezone'): if column_exists('users', 'timezone'):
op.execute("UPDATE users SET timezone = 'UTC' WHERE timezone IS NULL") op.execute("UPDATE users SET timezone = 'UTC' WHERE timezone IS NULL")
if column_exists('users', 'updated_at'):
op.execute("UPDATE users SET updated_at = datetime('now') WHERE updated_at IS NULL")
def downgrade() -> None: def downgrade() -> None:

View File

@ -1,5 +1,5 @@
from sqlalchemy import Column, Integer, String, DateTime from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql import func from datetime import datetime
from app.db.base import Base from app.db.base import Base
@ -15,5 +15,12 @@ class User(Base):
bio = Column(String, nullable=True) bio = Column(String, nullable=True)
preferred_language = Column(String, default="en") preferred_language = Column(String, default="en")
timezone = Column(String, default="UTC") timezone = Column(String, default="UTC")
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), nullable=True)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), nullable=True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.created_at:
self.created_at = datetime.utcnow()
if not self.updated_at:
self.updated_at = datetime.utcnow()

View File

@ -3,6 +3,7 @@ from sqlalchemy.orm import Session
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr
from typing import Optional from typing import Optional
import logging import logging
from datetime import datetime
from app.db.session import get_db from app.db.session import get_db
from app.models.user import User from app.models.user import User
from app.utils.auth import get_current_user, get_password_hash, verify_password from app.utils.auth import get_current_user, get_password_hash, verify_password
@ -90,6 +91,9 @@ async def update_profile(
if profile_data.timezone is not None: if profile_data.timezone is not None:
current_user.timezone = profile_data.timezone current_user.timezone = profile_data.timezone
# Update the timestamp manually
current_user.updated_at = datetime.utcnow()
db.commit() db.commit()
db.refresh(current_user) db.refresh(current_user)
@ -142,6 +146,7 @@ async def update_password(
# Update password # Update password
current_user.password_hash = get_password_hash(password_data.new_password) current_user.password_hash = get_password_hash(password_data.new_password)
current_user.updated_at = datetime.utcnow()
db.commit() db.commit()
logger.info(f"Password updated successfully for user: {current_user.email}") logger.info(f"Password updated successfully for user: {current_user.email}")
@ -185,6 +190,7 @@ async def update_email(
old_email = current_user.email old_email = current_user.email
current_user.email = email_data.new_email current_user.email = email_data.new_email
current_user.updated_at = datetime.utcnow()
db.commit() db.commit()
logger.info(f"Email updated successfully from {old_email} to {email_data.new_email}") logger.info(f"Email updated successfully from {old_email} to {email_data.new_email}")