Refactor: Reset Alembic migrations and consolidate models.
This commit addresses issues with backend models, schemas, and migrations. Key changes: - Consolidated all SQLAlchemy model definitions into `be/app/models.py`. - Emptied `be/app/models/expense.py` as its contents were duplicates. - Verified and standardized Base class usage and SQLAlchemy imports in models. - Confirmed the correctness of self-referential relationships in the `Expense` model. - Added a clarifying comment to `SplitTypeEnum` regarding future extensibility. - Corrected a typo in `Settlement.created_by_user_id`. Migration Cleanup: - Deleted all existing Alembic migration files from `be/alembic/versions/`. - Created a new, single initial migration script (`0001_initial_schema.py`) that defines the entire database schema based on the current state of the SQLAlchemy models. This provides a clean slate for future migrations. This reset was performed because the previous migration history was complex and contained a revision that was incompatible with the current model definitions. Starting fresh ensures consistency between the models and the database schema from the initial point.
This commit is contained in:
parent
813ed911f1
commit
a7fbc454a9
@ -1,9 +1,11 @@
|
|||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import asyncio # Add this import
|
||||||
|
|
||||||
from sqlalchemy import engine_from_config
|
from sqlalchemy import engine_from_config
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import pool
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine # Add this specific import
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
|
||||||
@ -22,11 +24,11 @@ from app.config import settings # Import settings to get DATABASE_URL
|
|||||||
config = context.config
|
config = context.config
|
||||||
|
|
||||||
# Set the sqlalchemy.url from your application settings
|
# Set the sqlalchemy.url from your application settings
|
||||||
# Use a synchronous version of the URL for Alembic's operations
|
# Ensure DATABASE_URL is available and use it directly
|
||||||
sync_db_url = settings.DATABASE_URL.replace("+asyncpg", "") if settings.DATABASE_URL else None
|
if not settings.DATABASE_URL:
|
||||||
if not sync_db_url:
|
|
||||||
raise ValueError("DATABASE_URL not found in settings for Alembic.")
|
raise ValueError("DATABASE_URL not found in settings for Alembic.")
|
||||||
config.set_main_option('sqlalchemy.url', sync_db_url)
|
config.set_main_option('sqlalchemy.url', settings.DATABASE_URL)
|
||||||
|
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
# Interpret the config file for Python logging.
|
||||||
# This line sets up loggers basically.
|
# This line sets up loggers basically.
|
||||||
@ -69,29 +71,37 @@ def run_migrations_offline() -> None:
|
|||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_online() -> None:
|
async def run_migrations_online_async() -> None: # Renamed and make async
|
||||||
"""Run migrations in 'online' mode.
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
In this scenario we need to create an Engine
|
In this scenario we need to create an Engine
|
||||||
and associate a connection with the context.
|
and associate a connection with the context.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
connectable = engine_from_config(
|
# connectable here will be an AsyncEngine if the URL is asyncpg
|
||||||
config.get_section(config.config_ini_section, {}),
|
db_url = config.get_main_option("sqlalchemy.url") # Get the async URL
|
||||||
prefix="sqlalchemy.",
|
if not db_url:
|
||||||
poolclass=pool.NullPool,
|
raise ValueError("Database URL is not configured in Alembic.")
|
||||||
)
|
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
connectable = create_async_engine(db_url, poolclass=pool.NullPool)
|
||||||
|
|
||||||
|
async with connectable.connect() as connection: # Use async with
|
||||||
|
# Pass target_metadata to the run_sync callback
|
||||||
|
await connection.run_sync(do_run_migrations, target_metadata)
|
||||||
|
|
||||||
|
await connectable.dispose() # Dispose of the async engine
|
||||||
|
|
||||||
|
def do_run_migrations(connection, metadata):
|
||||||
|
"""Helper function to configure and run migrations within a sync callback."""
|
||||||
context.configure(
|
context.configure(
|
||||||
connection=connection, target_metadata=target_metadata
|
connection=connection,
|
||||||
|
target_metadata=metadata
|
||||||
|
# Include other options like compare_type=True, compare_server_default=True if needed
|
||||||
)
|
)
|
||||||
|
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
if context.is_offline_mode():
|
if context.is_offline_mode():
|
||||||
run_migrations_offline()
|
run_migrations_offline()
|
||||||
else:
|
else:
|
||||||
run_migrations_online()
|
asyncio.run(run_migrations_online_async()) # Call the new async function
|
||||||
|
347
be/alembic/versions/0001_initial_schema.py
Normal file
347
be/alembic/versions/0001_initial_schema.py
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
"""Initial schema setup
|
||||||
|
|
||||||
|
Revision ID: 0001_initial_schema
|
||||||
|
Revises:
|
||||||
|
Create Date: YYYY-MM-DD HH:MM:SS.ffffff
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '0001_initial_schema'
|
||||||
|
down_revision: Union[str, None] = None
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
user_role_enum = postgresql.ENUM('owner', 'member', name='userroleenum', create_type=False)
|
||||||
|
split_type_enum = postgresql.ENUM('EQUAL', 'EXACT_AMOUNTS', 'PERCENTAGE', 'SHARES', 'ITEM_BASED', name='splittypeenum', create_type=False)
|
||||||
|
expense_split_status_enum = postgresql.ENUM('unpaid', 'partially_paid', 'paid', name='expensesplitstatusenum', create_type=False)
|
||||||
|
expense_overall_status_enum = postgresql.ENUM('unpaid', 'partially_paid', 'paid', name='expenseoverallstatusenum', create_type=False)
|
||||||
|
recurrence_type_enum = postgresql.ENUM('DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY', name='recurrencetypeenum', create_type=False)
|
||||||
|
chore_frequency_enum = postgresql.ENUM('one_time', 'daily', 'weekly', 'monthly', 'custom', name='chorefrequencyenum', create_type=False)
|
||||||
|
chore_type_enum = postgresql.ENUM('personal', 'group', name='choretypeenum', create_type=False)
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
user_role_enum.create(op.get_bind(), checkfirst=True)
|
||||||
|
split_type_enum.create(op.get_bind(), checkfirst=True)
|
||||||
|
expense_split_status_enum.create(op.get_bind(), checkfirst=True)
|
||||||
|
expense_overall_status_enum.create(op.get_bind(), checkfirst=True)
|
||||||
|
recurrence_type_enum.create(op.get_bind(), checkfirst=True)
|
||||||
|
chore_frequency_enum.create(op.get_bind(), checkfirst=True)
|
||||||
|
chore_type_enum.create(op.get_bind(), checkfirst=True)
|
||||||
|
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('email', sa.String(), nullable=False),
|
||||||
|
sa.Column('hashed_password', sa.String(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=True),
|
||||||
|
sa.Column('is_active', sa.Boolean(), server_default=sa.text('true'), nullable=False),
|
||||||
|
sa.Column('is_superuser', sa.Boolean(), server_default=sa.text('false'), nullable=False),
|
||||||
|
sa.Column('is_verified', sa.Boolean(), server_default=sa.text('false'), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
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_name'), 'users', ['name'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('groups',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_groups_id'), 'groups', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_groups_name'), 'groups', ['name'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('user_groups',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('group_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('role', user_role_enum, nullable=False),
|
||||||
|
sa.Column('joined_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('user_id', 'group_id', name='uq_user_group')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_user_groups_id'), 'user_groups', ['id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('invites',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('code', sa.String(), nullable=False),
|
||||||
|
sa.Column('group_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('is_active', sa.Boolean(), server_default=sa.text('true'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_invites_code'), 'invites', ['code'], unique=False)
|
||||||
|
op.create_index('ix_invites_active_code', 'invites', ['code'], unique=True, postgresql_where=sa.text('is_active = true'))
|
||||||
|
op.create_index(op.f('ix_invites_id'), 'invites', ['id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('lists',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('group_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('is_complete', sa.Boolean(), server_default=sa.text('false'), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('version', sa.Integer(), server_default='1', nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_lists_id'), 'lists', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_lists_name'), 'lists', ['name'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('items',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('list_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('quantity', sa.String(), nullable=True),
|
||||||
|
sa.Column('is_complete', sa.Boolean(), server_default=sa.text('false'), nullable=False),
|
||||||
|
sa.Column('price', sa.Numeric(precision=10, scale=2), nullable=True),
|
||||||
|
sa.Column('added_by_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('completed_by_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('version', sa.Integer(), server_default='1', nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['added_by_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['completed_by_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['list_id'], ['lists.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_items_id'), 'items', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_items_name'), 'items', ['name'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('recurrence_patterns',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('type', recurrence_type_enum, nullable=False),
|
||||||
|
sa.Column('interval', sa.Integer(), server_default='1', nullable=False),
|
||||||
|
sa.Column('days_of_week', sa.String(), nullable=True),
|
||||||
|
sa.Column('end_date', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('max_occurrences', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_recurrence_patterns_id'), 'recurrence_patterns', ['id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('expenses',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('description', sa.String(), nullable=False),
|
||||||
|
sa.Column('total_amount', sa.Numeric(precision=10, scale=2), nullable=False),
|
||||||
|
sa.Column('currency', sa.String(), server_default='USD', nullable=False),
|
||||||
|
sa.Column('expense_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('split_type', split_type_enum, nullable=False),
|
||||||
|
sa.Column('list_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('group_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('item_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('paid_by_user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_by_user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('version', sa.Integer(), server_default='1', nullable=False),
|
||||||
|
sa.Column('overall_settlement_status', expense_overall_status_enum, server_default='unpaid', nullable=False),
|
||||||
|
sa.Column('is_recurring', sa.Boolean(), server_default=sa.text('false'), nullable=False),
|
||||||
|
sa.Column('recurrence_pattern_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('next_occurrence', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('parent_expense_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('last_occurrence', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.CheckConstraint('(item_id IS NOT NULL) OR (list_id IS NOT NULL) OR (group_id IS NOT NULL)', name='chk_expense_context'),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_user_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['list_id'], ['lists.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['paid_by_user_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['parent_expense_id'], ['expenses.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['recurrence_pattern_id'], ['recurrence_patterns.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_expenses_created_by_user_id'), 'expenses', ['created_by_user_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_expenses_group_id'), 'expenses', ['group_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_expenses_id'), 'expenses', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_expenses_list_id'), 'expenses', ['list_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_expenses_paid_by_user_id'), 'expenses', ['paid_by_user_id'], unique=False)
|
||||||
|
op.create_index('ix_expenses_recurring_next_occurrence', 'expenses', ['is_recurring', 'next_occurrence'], unique=False, postgresql_where=sa.text('is_recurring = true'))
|
||||||
|
|
||||||
|
op.create_table('expense_splits',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('expense_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('owed_amount', sa.Numeric(precision=10, scale=2), nullable=False),
|
||||||
|
sa.Column('share_percentage', sa.Numeric(precision=5, scale=2), nullable=True),
|
||||||
|
sa.Column('share_units', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('status', expense_split_status_enum, server_default='unpaid', nullable=False),
|
||||||
|
sa.Column('paid_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['expense_id'], ['expenses.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('expense_id', 'user_id', name='uq_expense_user_split')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_expense_splits_id'), 'expense_splits', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_expense_splits_user_id'), 'expense_splits', ['user_id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('settlements',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('group_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('paid_by_user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('paid_to_user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('amount', sa.Numeric(precision=10, scale=2), nullable=False),
|
||||||
|
sa.Column('settlement_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_by_user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('version', sa.Integer(), server_default='1', nullable=False),
|
||||||
|
sa.CheckConstraint('paid_by_user_id != paid_to_user_id', name='chk_settlement_different_users'),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_user_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['paid_by_user_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['paid_to_user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_settlements_created_by_user_id'), 'settlements', ['created_by_user_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_settlements_group_id'), 'settlements', ['group_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_settlements_id'), 'settlements', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_settlements_paid_by_user_id'), 'settlements', ['paid_by_user_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_settlements_paid_to_user_id'), 'settlements', ['paid_to_user_id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('settlement_activities',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('expense_split_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('paid_by_user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('paid_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('amount_paid', sa.Numeric(precision=10, scale=2), nullable=False),
|
||||||
|
sa.Column('created_by_user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_user_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['expense_split_id'], ['expense_splits.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['paid_by_user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_settlement_activity_created_by_user_id'), 'settlement_activities', ['created_by_user_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_settlement_activity_expense_split_id'), 'settlement_activities', ['expense_split_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_settlement_activity_paid_by_user_id'), 'settlement_activities', ['paid_by_user_id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('chores',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('type', chore_type_enum, nullable=False),
|
||||||
|
sa.Column('group_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('frequency', chore_frequency_enum, nullable=False),
|
||||||
|
sa.Column('custom_interval_days', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('next_due_date', sa.Date(), nullable=False),
|
||||||
|
sa.Column('last_completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_chores_created_by_id'), 'chores', ['created_by_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_chores_group_id'), 'chores', ['group_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_chores_id'), 'chores', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_chores_name'), 'chores', ['name'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('chore_assignments',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('chore_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('assigned_to_user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('due_date', sa.Date(), nullable=False),
|
||||||
|
sa.Column('is_complete', sa.Boolean(), server_default=sa.text('false'), nullable=False),
|
||||||
|
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['assigned_to_user_id'], ['users.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['chore_id'], ['chores.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_chore_assignments_assigned_to_user_id'), 'chore_assignments', ['assigned_to_user_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_chore_assignments_chore_id'), 'chore_assignments', ['chore_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_chore_assignments_id'), 'chore_assignments', ['id'], unique=False)
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table('chore_assignments')
|
||||||
|
op.drop_index(op.f('ix_chores_name'), table_name='chores')
|
||||||
|
op.drop_index(op.f('ix_chores_id'), table_name='chores')
|
||||||
|
op.drop_index(op.f('ix_chores_group_id'), table_name='chores')
|
||||||
|
op.drop_index(op.f('ix_chores_created_by_id'), table_name='chores')
|
||||||
|
op.drop_table('chores')
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_settlement_activity_paid_by_user_id'), table_name='settlement_activities')
|
||||||
|
op.drop_index(op.f('ix_settlement_activity_expense_split_id'), table_name='settlement_activities')
|
||||||
|
op.drop_index(op.f('ix_settlement_activity_created_by_user_id'), table_name='settlement_activities')
|
||||||
|
op.drop_table('settlement_activities')
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_settlements_paid_to_user_id'), table_name='settlements')
|
||||||
|
op.drop_index(op.f('ix_settlements_paid_by_user_id'), table_name='settlements')
|
||||||
|
op.drop_index(op.f('ix_settlements_id'), table_name='settlements')
|
||||||
|
op.drop_index(op.f('ix_settlements_group_id'), table_name='settlements')
|
||||||
|
op.drop_index(op.f('ix_settlements_created_by_user_id'), table_name='settlements')
|
||||||
|
op.drop_table('settlements')
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_expense_splits_user_id'), table_name='expense_splits')
|
||||||
|
op.drop_index(op.f('ix_expense_splits_id'), table_name='expense_splits')
|
||||||
|
op.drop_table('expense_splits')
|
||||||
|
|
||||||
|
op.drop_index('ix_expenses_recurring_next_occurrence', table_name='expenses')
|
||||||
|
op.drop_index(op.f('ix_expenses_paid_by_user_id'), table_name='expenses')
|
||||||
|
op.drop_index(op.f('ix_expenses_list_id'), table_name='expenses')
|
||||||
|
op.drop_index(op.f('ix_expenses_id'), table_name='expenses')
|
||||||
|
op.drop_index(op.f('ix_expenses_group_id'), table_name='expenses')
|
||||||
|
op.drop_index(op.f('ix_expenses_created_by_user_id'), table_name='expenses')
|
||||||
|
op.drop_table('expenses')
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_recurrence_patterns_id'), table_name='recurrence_patterns')
|
||||||
|
op.drop_table('recurrence_patterns')
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_items_name'), table_name='items')
|
||||||
|
op.drop_index(op.f('ix_items_id'), table_name='items')
|
||||||
|
op.drop_table('items')
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_lists_name'), table_name='lists')
|
||||||
|
op.drop_index(op.f('ix_lists_id'), table_name='lists')
|
||||||
|
op.drop_table('lists')
|
||||||
|
|
||||||
|
op.drop_index('ix_invites_active_code', table_name='invites')
|
||||||
|
op.drop_index(op.f('ix_invites_id'), table_name='invites')
|
||||||
|
op.drop_index(op.f('ix_invites_code'), table_name='invites')
|
||||||
|
op.drop_table('invites')
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_user_groups_id'), table_name='user_groups')
|
||||||
|
op.drop_table('user_groups')
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_groups_name'), table_name='groups')
|
||||||
|
op.drop_index(op.f('ix_groups_id'), table_name='groups')
|
||||||
|
op.drop_table('groups')
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_users_name'), table_name='users')
|
||||||
|
op.drop_index(op.f('ix_users_id'), table_name='users')
|
||||||
|
op.drop_index(op.f('ix_users_email'), table_name='users')
|
||||||
|
op.drop_table('users')
|
||||||
|
|
||||||
|
chore_type_enum.drop(op.get_bind(), checkfirst=False)
|
||||||
|
chore_frequency_enum.drop(op.get_bind(), checkfirst=False)
|
||||||
|
recurrence_type_enum.drop(op.get_bind(), checkfirst=False)
|
||||||
|
expense_overall_status_enum.drop(op.get_bind(), checkfirst=False)
|
||||||
|
expense_split_status_enum.drop(op.get_bind(), checkfirst=False)
|
||||||
|
split_type_enum.drop(op.get_bind(), checkfirst=False)
|
||||||
|
user_role_enum.drop(op.get_bind(), checkfirst=False)
|
@ -1,90 +0,0 @@
|
|||||||
"""add_recurrence_pattern
|
|
||||||
|
|
||||||
Revision ID: 295cb070f266
|
|
||||||
Revises: 7cc1484074eb
|
|
||||||
Create Date: 2025-05-22 19:55:24.650524
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '295cb070f266'
|
|
||||||
down_revision: Union[str, None] = '7cc1484074eb'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.alter_column('expenses', 'next_occurrence',
|
|
||||||
existing_type=postgresql.TIMESTAMP(),
|
|
||||||
type_=sa.DateTime(timezone=True),
|
|
||||||
existing_nullable=True)
|
|
||||||
op.drop_index('ix_expenses_recurring_next_occurrence', table_name='expenses', postgresql_where='(is_recurring = true)')
|
|
||||||
op.drop_constraint('fk_expenses_recurrence_pattern_id', 'expenses', type_='foreignkey')
|
|
||||||
op.drop_constraint('fk_expenses_parent_expense_id', 'expenses', type_='foreignkey')
|
|
||||||
op.drop_column('expenses', 'recurrence_pattern_id')
|
|
||||||
op.drop_column('expenses', 'last_occurrence')
|
|
||||||
op.drop_column('expenses', 'parent_expense_id')
|
|
||||||
op.alter_column('recurrence_patterns', 'days_of_week',
|
|
||||||
existing_type=postgresql.JSON(astext_type=sa.Text()),
|
|
||||||
type_=sa.String(),
|
|
||||||
existing_nullable=True)
|
|
||||||
op.alter_column('recurrence_patterns', 'end_date',
|
|
||||||
existing_type=postgresql.TIMESTAMP(),
|
|
||||||
type_=sa.DateTime(timezone=True),
|
|
||||||
existing_nullable=True)
|
|
||||||
op.alter_column('recurrence_patterns', 'created_at',
|
|
||||||
existing_type=postgresql.TIMESTAMP(),
|
|
||||||
type_=sa.DateTime(timezone=True),
|
|
||||||
existing_nullable=False)
|
|
||||||
op.alter_column('recurrence_patterns', 'updated_at',
|
|
||||||
existing_type=postgresql.TIMESTAMP(),
|
|
||||||
type_=sa.DateTime(timezone=True),
|
|
||||||
existing_nullable=False)
|
|
||||||
op.create_index(op.f('ix_settlement_activities_created_by_user_id'), 'settlement_activities', ['created_by_user_id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_settlement_activities_expense_split_id'), 'settlement_activities', ['expense_split_id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_settlement_activities_id'), 'settlement_activities', ['id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_settlement_activities_paid_by_user_id'), 'settlement_activities', ['paid_by_user_id'], unique=False)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_index(op.f('ix_settlement_activities_paid_by_user_id'), table_name='settlement_activities')
|
|
||||||
op.drop_index(op.f('ix_settlement_activities_id'), table_name='settlement_activities')
|
|
||||||
op.drop_index(op.f('ix_settlement_activities_expense_split_id'), table_name='settlement_activities')
|
|
||||||
op.drop_index(op.f('ix_settlement_activities_created_by_user_id'), table_name='settlement_activities')
|
|
||||||
op.alter_column('recurrence_patterns', 'updated_at',
|
|
||||||
existing_type=sa.DateTime(timezone=True),
|
|
||||||
type_=postgresql.TIMESTAMP(),
|
|
||||||
existing_nullable=False)
|
|
||||||
op.alter_column('recurrence_patterns', 'created_at',
|
|
||||||
existing_type=sa.DateTime(timezone=True),
|
|
||||||
type_=postgresql.TIMESTAMP(),
|
|
||||||
existing_nullable=False)
|
|
||||||
op.alter_column('recurrence_patterns', 'end_date',
|
|
||||||
existing_type=sa.DateTime(timezone=True),
|
|
||||||
type_=postgresql.TIMESTAMP(),
|
|
||||||
existing_nullable=True)
|
|
||||||
op.alter_column('recurrence_patterns', 'days_of_week',
|
|
||||||
existing_type=sa.String(),
|
|
||||||
type_=postgresql.JSON(astext_type=sa.Text()),
|
|
||||||
existing_nullable=True)
|
|
||||||
op.add_column('expenses', sa.Column('parent_expense_id', sa.INTEGER(), autoincrement=False, nullable=True))
|
|
||||||
op.add_column('expenses', sa.Column('last_occurrence', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
|
|
||||||
op.add_column('expenses', sa.Column('recurrence_pattern_id', sa.INTEGER(), autoincrement=False, nullable=True))
|
|
||||||
op.create_foreign_key('fk_expenses_parent_expense_id', 'expenses', 'expenses', ['parent_expense_id'], ['id'], ondelete='SET NULL')
|
|
||||||
op.create_foreign_key('fk_expenses_recurrence_pattern_id', 'expenses', 'recurrence_patterns', ['recurrence_pattern_id'], ['id'], ondelete='SET NULL')
|
|
||||||
op.create_index('ix_expenses_recurring_next_occurrence', 'expenses', ['is_recurring', 'next_occurrence'], unique=False, postgresql_where='(is_recurring = true)')
|
|
||||||
op.alter_column('expenses', 'next_occurrence',
|
|
||||||
existing_type=sa.DateTime(timezone=True),
|
|
||||||
type_=postgresql.TIMESTAMP(),
|
|
||||||
existing_nullable=True)
|
|
||||||
# ### end Alembic commands ###
|
|
@ -1,42 +0,0 @@
|
|||||||
"""Initial database schema
|
|
||||||
|
|
||||||
Revision ID: 5271d18372e5
|
|
||||||
Revises: 5e8b6dde50fc
|
|
||||||
Create Date: 2025-05-17 14:39:03.690180
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '5271d18372e5'
|
|
||||||
down_revision: Union[str, None] = '5e8b6dde50fc'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.add_column('expenses', sa.Column('created_by_user_id', sa.Integer(), nullable=False))
|
|
||||||
op.create_index(op.f('ix_expenses_created_by_user_id'), 'expenses', ['created_by_user_id'], unique=False)
|
|
||||||
op.create_foreign_key(None, 'expenses', 'users', ['created_by_user_id'], ['id'])
|
|
||||||
op.add_column('settlements', sa.Column('created_by_user_id', sa.Integer(), nullable=False))
|
|
||||||
op.create_index(op.f('ix_settlements_created_by_user_id'), 'settlements', ['created_by_user_id'], unique=False)
|
|
||||||
op.create_foreign_key(None, 'settlements', 'users', ['created_by_user_id'], ['id'])
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_constraint(None, 'settlements', type_='foreignkey')
|
|
||||||
op.drop_index(op.f('ix_settlements_created_by_user_id'), table_name='settlements')
|
|
||||||
op.drop_column('settlements', 'created_by_user_id')
|
|
||||||
op.drop_constraint(None, 'expenses', type_='foreignkey')
|
|
||||||
op.drop_index(op.f('ix_expenses_created_by_user_id'), table_name='expenses')
|
|
||||||
op.drop_column('expenses', 'created_by_user_id')
|
|
||||||
# ### end Alembic commands ###
|
|
@ -1,62 +0,0 @@
|
|||||||
"""update_user_model_for_fastapi_users
|
|
||||||
|
|
||||||
Revision ID: 5e8b6dde50fc
|
|
||||||
Revises: 7c26d62e8005
|
|
||||||
Create Date: 2025-05-13 23:30:02.005611
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
import secrets
|
|
||||||
from passlib.context import CryptContext
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '5e8b6dde50fc'
|
|
||||||
down_revision: Union[str, None] = '7c26d62e8005'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# Create password hasher
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
||||||
|
|
||||||
# Generate a secure random password and hash it
|
|
||||||
random_password = secrets.token_urlsafe(32) # 32 bytes of randomness
|
|
||||||
secure_hash = pwd_context.hash(random_password)
|
|
||||||
|
|
||||||
# 1. Add columns as nullable or with a default
|
|
||||||
op.add_column('users', sa.Column('hashed_password', sa.String(), nullable=True))
|
|
||||||
op.add_column('users', sa.Column('is_active', sa.Boolean(), nullable=True, server_default=sa.sql.expression.true()))
|
|
||||||
op.add_column('users', sa.Column('is_superuser', sa.Boolean(), nullable=True, server_default=sa.sql.expression.false()))
|
|
||||||
op.add_column('users', sa.Column('is_verified', sa.Boolean(), nullable=True, server_default=sa.sql.expression.false()))
|
|
||||||
|
|
||||||
# 2. Set default values for existing rows with secure hash
|
|
||||||
op.execute(f"UPDATE users SET hashed_password = '{secure_hash}' WHERE hashed_password IS NULL")
|
|
||||||
op.execute("UPDATE users SET is_active = true WHERE is_active IS NULL")
|
|
||||||
op.execute("UPDATE users SET is_superuser = false WHERE is_superuser IS NULL")
|
|
||||||
op.execute("UPDATE users SET is_verified = false WHERE is_verified IS NULL")
|
|
||||||
|
|
||||||
# 3. Alter columns to be non-nullable
|
|
||||||
op.alter_column('users', 'hashed_password', nullable=False)
|
|
||||||
op.alter_column('users', 'is_active', nullable=False)
|
|
||||||
op.alter_column('users', 'is_superuser', nullable=False)
|
|
||||||
op.alter_column('users', 'is_verified', nullable=False)
|
|
||||||
|
|
||||||
# 4. Drop the old column
|
|
||||||
op.drop_column('users', 'password_hash')
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.add_column('users', sa.Column('password_hash', sa.VARCHAR(), autoincrement=False, nullable=False))
|
|
||||||
op.drop_column('users', 'is_verified')
|
|
||||||
op.drop_column('users', 'is_superuser')
|
|
||||||
op.drop_column('users', 'is_active')
|
|
||||||
op.drop_column('users', 'hashed_password')
|
|
||||||
# ### end Alembic commands ###
|
|
@ -1,32 +0,0 @@
|
|||||||
"""Initial database schema
|
|
||||||
|
|
||||||
Revision ID: 5ed3ccbf05f7
|
|
||||||
Revises: 5271d18372e5
|
|
||||||
Create Date: 2025-05-17 14:40:52.165607
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '5ed3ccbf05f7'
|
|
||||||
down_revision: Union[str, None] = '5271d18372e5'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
@ -1,60 +0,0 @@
|
|||||||
"""add_missing_indexes_and_constraints
|
|
||||||
|
|
||||||
Revision ID: 7c26d62e8005
|
|
||||||
Revises: bc37e9c7ae19
|
|
||||||
Create Date: 2025-05-13 21:44:46.408395
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '7c26d62e8005'
|
|
||||||
down_revision: Union[str, None] = 'bc37e9c7ae19'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_index('ix_expense_splits_user_id', 'expense_splits', ['user_id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_expenses_group_id'), 'expenses', ['group_id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_expenses_list_id'), 'expenses', ['list_id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_expenses_paid_by_user_id'), 'expenses', ['paid_by_user_id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_settlements_group_id'), 'settlements', ['group_id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_settlements_paid_by_user_id'), 'settlements', ['paid_by_user_id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_settlements_paid_to_user_id'), 'settlements', ['paid_to_user_id'], unique=False)
|
|
||||||
|
|
||||||
# Add check constraints
|
|
||||||
op.create_check_constraint(
|
|
||||||
'chk_expense_context',
|
|
||||||
'expenses',
|
|
||||||
'(item_id IS NOT NULL) OR (list_id IS NOT NULL) OR (group_id IS NOT NULL)'
|
|
||||||
)
|
|
||||||
op.create_check_constraint(
|
|
||||||
'chk_settlement_different_users',
|
|
||||||
'settlements',
|
|
||||||
'paid_by_user_id != paid_to_user_id'
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
# Drop check constraints
|
|
||||||
op.drop_constraint('chk_settlement_different_users', 'settlements', type_='check')
|
|
||||||
op.drop_constraint('chk_expense_context', 'expenses', type_='check')
|
|
||||||
|
|
||||||
op.drop_index(op.f('ix_settlements_paid_to_user_id'), table_name='settlements')
|
|
||||||
op.drop_index(op.f('ix_settlements_paid_by_user_id'), table_name='settlements')
|
|
||||||
op.drop_index(op.f('ix_settlements_group_id'), table_name='settlements')
|
|
||||||
op.drop_index(op.f('ix_expenses_paid_by_user_id'), table_name='expenses')
|
|
||||||
op.drop_index(op.f('ix_expenses_list_id'), table_name='expenses')
|
|
||||||
op.drop_index(op.f('ix_expenses_group_id'), table_name='expenses')
|
|
||||||
op.drop_index('ix_expense_splits_user_id', table_name='expense_splits')
|
|
||||||
# ### end Alembic commands ###
|
|
@ -1,28 +0,0 @@
|
|||||||
"""merge heads
|
|
||||||
|
|
||||||
Revision ID: 7cc1484074eb
|
|
||||||
Revises: add_recurring_expenses, e981855d0418
|
|
||||||
Create Date: 2025-05-22 16:11:32.030039
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '7cc1484074eb'
|
|
||||||
down_revision: Union[str, None] = ('add_recurring_expenses', 'e981855d0418')
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
pass
|
|
@ -1,32 +0,0 @@
|
|||||||
"""check_models_alignment
|
|
||||||
|
|
||||||
Revision ID: 8efbdc779a76
|
|
||||||
Revises: 5ed3ccbf05f7
|
|
||||||
Create Date: 2025-05-17 15:03:08.242908
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '8efbdc779a76'
|
|
||||||
down_revision: Union[str, None] = '5ed3ccbf05f7'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
@ -1,80 +0,0 @@
|
|||||||
"""add recurring expenses
|
|
||||||
|
|
||||||
Revision ID: add_recurring_expenses
|
|
||||||
Revises: # You'll need to update this with your latest migration
|
|
||||||
Create Date: 2024-03-19 10:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'add_recurring_expenses'
|
|
||||||
down_revision = None # Update this with your latest migration
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# Create recurrence_patterns table
|
|
||||||
op.create_table(
|
|
||||||
'recurrence_patterns',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('type', sa.String(), nullable=False),
|
|
||||||
sa.Column('interval', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('days_of_week', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
|
||||||
sa.Column('end_date', sa.DateTime(), nullable=True),
|
|
||||||
sa.Column('max_occurrences', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_recurrence_patterns_id'), 'recurrence_patterns', ['id'], unique=False)
|
|
||||||
|
|
||||||
# Add recurring expense columns to expenses table
|
|
||||||
op.add_column('expenses', sa.Column('is_recurring', sa.Boolean(), nullable=False, server_default='false'))
|
|
||||||
op.add_column('expenses', sa.Column('next_occurrence', sa.DateTime(), nullable=True))
|
|
||||||
op.add_column('expenses', sa.Column('last_occurrence', sa.DateTime(), nullable=True))
|
|
||||||
op.add_column('expenses', sa.Column('recurrence_pattern_id', sa.Integer(), nullable=True))
|
|
||||||
op.add_column('expenses', sa.Column('parent_expense_id', sa.Integer(), nullable=True))
|
|
||||||
|
|
||||||
# Add foreign key constraints
|
|
||||||
op.create_foreign_key(
|
|
||||||
'fk_expenses_recurrence_pattern_id',
|
|
||||||
'expenses', 'recurrence_patterns',
|
|
||||||
['recurrence_pattern_id'], ['id'],
|
|
||||||
ondelete='SET NULL'
|
|
||||||
)
|
|
||||||
op.create_foreign_key(
|
|
||||||
'fk_expenses_parent_expense_id',
|
|
||||||
'expenses', 'expenses',
|
|
||||||
['parent_expense_id'], ['id'],
|
|
||||||
ondelete='SET NULL'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add indexes
|
|
||||||
op.create_index(
|
|
||||||
'ix_expenses_recurring_next_occurrence',
|
|
||||||
'expenses',
|
|
||||||
['is_recurring', 'next_occurrence'],
|
|
||||||
postgresql_where=sa.text('is_recurring = true')
|
|
||||||
)
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# Drop indexes
|
|
||||||
op.drop_index('ix_expenses_recurring_next_occurrence', table_name='expenses')
|
|
||||||
|
|
||||||
# Drop foreign key constraints
|
|
||||||
op.drop_constraint('fk_expenses_parent_expense_id', 'expenses', type_='foreignkey')
|
|
||||||
op.drop_constraint('fk_expenses_recurrence_pattern_id', 'expenses', type_='foreignkey')
|
|
||||||
|
|
||||||
# Drop columns from expenses table
|
|
||||||
op.drop_column('expenses', 'parent_expense_id')
|
|
||||||
op.drop_column('expenses', 'recurrence_pattern_id')
|
|
||||||
op.drop_column('expenses', 'last_occurrence')
|
|
||||||
op.drop_column('expenses', 'next_occurrence')
|
|
||||||
op.drop_column('expenses', 'is_recurring')
|
|
||||||
|
|
||||||
# Drop recurrence_patterns table
|
|
||||||
op.drop_index(op.f('ix_recurrence_patterns_id'), table_name='recurrence_patterns')
|
|
||||||
op.drop_table('recurrence_patterns')
|
|
@ -1,191 +0,0 @@
|
|||||||
"""fresh start
|
|
||||||
|
|
||||||
Revision ID: bc37e9c7ae19
|
|
||||||
Revises:
|
|
||||||
Create Date: 2025-05-08 16:06:51.208542
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = 'bc37e9c7ae19'
|
|
||||||
down_revision: Union[str, None] = None
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('users',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('email', sa.String(), nullable=False),
|
|
||||||
sa.Column('password_hash', sa.String(), nullable=False),
|
|
||||||
sa.Column('name', sa.String(), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
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_name'), 'users', ['name'], unique=False)
|
|
||||||
op.create_table('groups',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('name', sa.String(), nullable=False),
|
|
||||||
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_groups_id'), 'groups', ['id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_groups_name'), 'groups', ['name'], unique=False)
|
|
||||||
op.create_table('invites',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('code', sa.String(), nullable=False),
|
|
||||||
sa.Column('group_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
|
|
||||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE'),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index('ix_invites_active_code', 'invites', ['code'], unique=True, postgresql_where=sa.text('is_active = true'))
|
|
||||||
op.create_index(op.f('ix_invites_code'), 'invites', ['code'], unique=False)
|
|
||||||
op.create_index(op.f('ix_invites_id'), 'invites', ['id'], unique=False)
|
|
||||||
op.create_table('lists',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('name', sa.String(), nullable=False),
|
|
||||||
sa.Column('description', sa.Text(), nullable=True),
|
|
||||||
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('group_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('is_complete', sa.Boolean(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('version', sa.Integer(), server_default='1', nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_lists_id'), 'lists', ['id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_lists_name'), 'lists', ['name'], unique=False)
|
|
||||||
op.create_table('settlements',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('group_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('paid_by_user_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('paid_to_user_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('amount', sa.Numeric(precision=10, scale=2), nullable=False),
|
|
||||||
sa.Column('settlement_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('description', sa.Text(), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('version', sa.Integer(), server_default='1', nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['paid_by_user_id'], ['users.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['paid_to_user_id'], ['users.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_settlements_id'), 'settlements', ['id'], unique=False)
|
|
||||||
op.create_table('user_groups',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('group_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('role', sa.Enum('owner', 'member', name='userroleenum'), nullable=False),
|
|
||||||
sa.Column('joined_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE'),
|
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.UniqueConstraint('user_id', 'group_id', name='uq_user_group')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_user_groups_id'), 'user_groups', ['id'], unique=False)
|
|
||||||
op.create_table('items',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('list_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('name', sa.String(), nullable=False),
|
|
||||||
sa.Column('quantity', sa.String(), nullable=True),
|
|
||||||
sa.Column('is_complete', sa.Boolean(), nullable=False),
|
|
||||||
sa.Column('price', sa.Numeric(precision=10, scale=2), nullable=True),
|
|
||||||
sa.Column('added_by_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('completed_by_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('version', sa.Integer(), server_default='1', nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['added_by_id'], ['users.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['completed_by_id'], ['users.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['list_id'], ['lists.id'], ondelete='CASCADE'),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_items_id'), 'items', ['id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_items_name'), 'items', ['name'], unique=False)
|
|
||||||
op.create_table('expenses',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('description', sa.String(), nullable=False),
|
|
||||||
sa.Column('total_amount', sa.Numeric(precision=10, scale=2), nullable=False),
|
|
||||||
sa.Column('currency', sa.String(), nullable=False),
|
|
||||||
sa.Column('expense_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('split_type', sa.Enum('EQUAL', 'EXACT_AMOUNTS', 'PERCENTAGE', 'SHARES', 'ITEM_BASED', name='splittypeenum'), nullable=False),
|
|
||||||
sa.Column('list_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('group_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('item_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('paid_by_user_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('version', sa.Integer(), server_default='1', nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['list_id'], ['lists.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['paid_by_user_id'], ['users.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_expenses_id'), 'expenses', ['id'], unique=False)
|
|
||||||
op.create_table('expense_splits',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('expense_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('owed_amount', sa.Numeric(precision=10, scale=2), nullable=False),
|
|
||||||
sa.Column('share_percentage', sa.Numeric(precision=5, scale=2), nullable=True),
|
|
||||||
sa.Column('share_units', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['expense_id'], ['expenses.id'], ondelete='CASCADE'),
|
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.UniqueConstraint('expense_id', 'user_id', name='uq_expense_user_split')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_expense_splits_id'), 'expense_splits', ['id'], unique=False)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_index(op.f('ix_expense_splits_id'), table_name='expense_splits')
|
|
||||||
op.drop_table('expense_splits')
|
|
||||||
op.drop_index(op.f('ix_expenses_id'), table_name='expenses')
|
|
||||||
op.drop_table('expenses')
|
|
||||||
op.drop_index(op.f('ix_items_name'), table_name='items')
|
|
||||||
op.drop_index(op.f('ix_items_id'), table_name='items')
|
|
||||||
op.drop_table('items')
|
|
||||||
op.drop_index(op.f('ix_user_groups_id'), table_name='user_groups')
|
|
||||||
op.drop_table('user_groups')
|
|
||||||
op.drop_index(op.f('ix_settlements_id'), table_name='settlements')
|
|
||||||
op.drop_table('settlements')
|
|
||||||
op.drop_index(op.f('ix_lists_name'), table_name='lists')
|
|
||||||
op.drop_index(op.f('ix_lists_id'), table_name='lists')
|
|
||||||
op.drop_table('lists')
|
|
||||||
op.drop_index(op.f('ix_invites_id'), table_name='invites')
|
|
||||||
op.drop_index(op.f('ix_invites_code'), table_name='invites')
|
|
||||||
op.drop_index('ix_invites_active_code', table_name='invites', postgresql_where=sa.text('is_active = true'))
|
|
||||||
op.drop_table('invites')
|
|
||||||
op.drop_index(op.f('ix_groups_name'), table_name='groups')
|
|
||||||
op.drop_index(op.f('ix_groups_id'), table_name='groups')
|
|
||||||
op.drop_table('groups')
|
|
||||||
op.drop_index(op.f('ix_users_name'), table_name='users')
|
|
||||||
op.drop_index(op.f('ix_users_id'), table_name='users')
|
|
||||||
op.drop_index(op.f('ix_users_email'), table_name='users')
|
|
||||||
op.drop_table('users')
|
|
||||||
# ### end Alembic commands ###
|
|
@ -1,82 +0,0 @@
|
|||||||
"""add_settlement_activity_and_status_fields
|
|
||||||
|
|
||||||
Revision ID: e981855d0418
|
|
||||||
Revises: manual_0002
|
|
||||||
Create Date: 2025-05-22 02:13:06.419914
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = 'e981855d0418'
|
|
||||||
down_revision: Union[str, None] = 'manual_0002'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
# Define Enum types for use in upgrade and downgrade
|
|
||||||
expense_split_status_enum = postgresql.ENUM('unpaid', 'partially_paid', 'paid', name='expensesplitstatusenum')
|
|
||||||
expense_overall_status_enum = postgresql.ENUM('unpaid', 'partially_paid', 'paid', name='expenseoverallstatusenum')
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
|
|
||||||
# Create ENUM types
|
|
||||||
expense_split_status_enum.create(op.get_bind(), checkfirst=True)
|
|
||||||
expense_overall_status_enum.create(op.get_bind(), checkfirst=True)
|
|
||||||
|
|
||||||
# Add 'overall_settlement_status' column to 'expenses' table
|
|
||||||
op.add_column('expenses', sa.Column('overall_settlement_status', expense_overall_status_enum, server_default='unpaid', nullable=False))
|
|
||||||
|
|
||||||
# Add 'status' and 'paid_at' columns to 'expense_splits' table
|
|
||||||
op.add_column('expense_splits', sa.Column('status', expense_split_status_enum, server_default='unpaid', nullable=False))
|
|
||||||
op.add_column('expense_splits', sa.Column('paid_at', sa.DateTime(timezone=True), nullable=True))
|
|
||||||
|
|
||||||
# Create 'settlement_activities' table
|
|
||||||
op.create_table('settlement_activities',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('expense_split_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('paid_by_user_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('paid_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('amount_paid', sa.Numeric(precision=10, scale=2), nullable=False),
|
|
||||||
sa.Column('created_by_user_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), # Removed onupdate for initial creation
|
|
||||||
sa.ForeignKeyConstraint(['created_by_user_id'], ['users.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['expense_split_id'], ['expense_splits.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['paid_by_user_id'], ['users.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_settlement_activity_created_by_user_id'), 'settlement_activities', ['created_by_user_id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_settlement_activity_expense_split_id'), 'settlement_activities', ['expense_split_id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_settlement_activity_paid_by_user_id'), 'settlement_activities', ['paid_by_user_id'], unique=False)
|
|
||||||
|
|
||||||
# Manually add onupdate trigger for updated_at as Alembic doesn't handle it well for all DBs
|
|
||||||
# For PostgreSQL, this is typically done via a trigger function.
|
|
||||||
# However, for simplicity in this migration, we rely on the application layer to update this field.
|
|
||||||
# Or, if using a database that supports it directly in Column definition (like some newer SQLAlch versions for certain backends):
|
|
||||||
# op.alter_column('settlement_activities', 'updated_at', server_default=sa.text('now()'), onupdate=sa.text('now()'))
|
|
||||||
# For now, the model has onupdate=func.now(), which SQLAlchemy ORM handles. The DDL here is for initial creation.
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_index(op.f('ix_settlement_activity_paid_by_user_id'), table_name='settlement_activities')
|
|
||||||
op.drop_index(op.f('ix_settlement_activity_expense_split_id'), table_name='settlement_activities')
|
|
||||||
op.drop_index(op.f('ix_settlement_activity_created_by_user_id'), table_name='settlement_activities')
|
|
||||||
op.drop_table('settlement_activities')
|
|
||||||
|
|
||||||
op.drop_column('expense_splits', 'paid_at')
|
|
||||||
op.drop_column('expense_splits', 'status')
|
|
||||||
|
|
||||||
op.drop_column('expenses', 'overall_settlement_status')
|
|
||||||
|
|
||||||
# Drop ENUM types
|
|
||||||
expense_split_status_enum.drop(op.get_bind(), checkfirst=False)
|
|
||||||
expense_overall_status_enum.drop(op.get_bind(), checkfirst=False)
|
|
||||||
# ### end Alembic commands ###
|
|
@ -1,78 +0,0 @@
|
|||||||
"""manual_0001_add_chore_tables
|
|
||||||
|
|
||||||
Revision ID: manual_0001
|
|
||||||
Revises: 8efbdc779a76
|
|
||||||
Create Date: 2025-05-21 08:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = 'manual_0001'
|
|
||||||
down_revision: Union[str, None] = '8efbdc779a76' # Last real migration
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
# Enum definition for ChoreFrequencyEnum
|
|
||||||
chore_frequency_enum = postgresql.ENUM('one_time', 'daily', 'weekly', 'monthly', 'custom', name='chorefrequencyenum', create_type=False)
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# Create chorefrequencyenum type if it doesn't exist
|
|
||||||
connection = op.get_bind()
|
|
||||||
if not connection.dialect.has_type(connection, 'chorefrequencyenum'):
|
|
||||||
chore_frequency_enum.create(connection)
|
|
||||||
|
|
||||||
# Create chores table
|
|
||||||
op.create_table('chores',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('group_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('name', sa.String(), nullable=False),
|
|
||||||
sa.Column('description', sa.Text(), nullable=True),
|
|
||||||
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('frequency', chore_frequency_enum, nullable=False),
|
|
||||||
sa.Column('custom_interval_days', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('next_due_date', sa.Date(), nullable=False),
|
|
||||||
sa.Column('last_completed_at', sa.DateTime(timezone=True), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), onupdate=sa.text('now()'), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create indexes for chores table
|
|
||||||
op.create_index('ix_chores_created_by_id', 'chores', ['created_by_id'], unique=False)
|
|
||||||
op.create_index('ix_chores_group_id', 'chores', ['group_id'], unique=False)
|
|
||||||
op.create_index('ix_chores_id', 'chores', ['id'], unique=False)
|
|
||||||
op.create_index('ix_chores_name', 'chores', ['name'], unique=False)
|
|
||||||
|
|
||||||
# Create chore_assignments table
|
|
||||||
op.create_table('chore_assignments',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('chore_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('assigned_to_user_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('due_date', sa.Date(), nullable=False),
|
|
||||||
sa.Column('is_complete', sa.Boolean(), server_default=sa.false(), nullable=False),
|
|
||||||
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), onupdate=sa.text('now()'), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.ForeignKeyConstraint(['assigned_to_user_id'], ['users.id'], ondelete='CASCADE'),
|
|
||||||
sa.ForeignKeyConstraint(['chore_id'], ['chores.id'], ondelete='CASCADE'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create indexes for chore_assignments table
|
|
||||||
op.create_index('ix_chore_assignments_assigned_to_user_id', 'chore_assignments', ['assigned_to_user_id'], unique=False)
|
|
||||||
op.create_index('ix_chore_assignments_chore_id', 'chore_assignments', ['chore_id'], unique=False)
|
|
||||||
op.create_index('ix_chore_assignments_id', 'chore_assignments', ['id'], unique=False)
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
op.drop_table('chore_assignments')
|
|
||||||
op.drop_table('chores')
|
|
||||||
# Don't drop the enum type as it might be used by other tables
|
|
@ -1,60 +0,0 @@
|
|||||||
"""manual_0002_add_personal_chores
|
|
||||||
|
|
||||||
Revision ID: manual_0002
|
|
||||||
Revises: manual_0001
|
|
||||||
Create Date: 2025-05-22 08:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = 'manual_0002'
|
|
||||||
down_revision: Union[str, None] = 'manual_0001'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
# Enum definition for ChoreTypeEnum
|
|
||||||
chore_type_enum = postgresql.ENUM('personal', 'group', name='choretypeenum', create_type=False)
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# Create choretypeenum type if it doesn't exist
|
|
||||||
connection = op.get_bind()
|
|
||||||
if not connection.dialect.has_type(connection, 'choretypeenum'):
|
|
||||||
chore_type_enum.create(connection)
|
|
||||||
|
|
||||||
# Add type column and make group_id nullable
|
|
||||||
op.add_column('chores', sa.Column('type', chore_type_enum, nullable=True))
|
|
||||||
op.alter_column('chores', 'group_id',
|
|
||||||
existing_type=sa.Integer(),
|
|
||||||
nullable=True,
|
|
||||||
existing_server_default=None
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set default type for existing chores
|
|
||||||
op.execute("UPDATE chores SET type = 'group' WHERE type IS NULL")
|
|
||||||
|
|
||||||
# Make type column non-nullable after setting defaults
|
|
||||||
op.alter_column('chores', 'type',
|
|
||||||
existing_type=chore_type_enum,
|
|
||||||
nullable=False,
|
|
||||||
existing_server_default=None
|
|
||||||
)
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# Make group_id non-nullable again
|
|
||||||
op.alter_column('chores', 'group_id',
|
|
||||||
existing_type=sa.Integer(),
|
|
||||||
nullable=False,
|
|
||||||
existing_server_default=None
|
|
||||||
)
|
|
||||||
|
|
||||||
# Remove type column
|
|
||||||
op.drop_column('chores', 'type')
|
|
||||||
|
|
||||||
# Don't drop the enum type as it might be used by other tables
|
|
@ -38,6 +38,8 @@ class SplitTypeEnum(enum.Enum):
|
|||||||
PERCENTAGE = "PERCENTAGE" # Percentage for each user (defined in ExpenseSplit)
|
PERCENTAGE = "PERCENTAGE" # Percentage for each user (defined in ExpenseSplit)
|
||||||
SHARES = "SHARES" # Proportional to shares/units (defined in ExpenseSplit)
|
SHARES = "SHARES" # Proportional to shares/units (defined in ExpenseSplit)
|
||||||
ITEM_BASED = "ITEM_BASED" # If an expense is derived directly from item prices and who added them
|
ITEM_BASED = "ITEM_BASED" # If an expense is derived directly from item prices and who added them
|
||||||
|
# Consider renaming to a more generic term like 'DERIVED' or 'ENTITY_DRIVEN'
|
||||||
|
# if expenses might be derived from other entities in the future.
|
||||||
# Add more types as needed, e.g., UNPAID (for tracking debts not part of a formal expense)
|
# Add more types as needed, e.g., UNPAID (for tracking debts not part of a formal expense)
|
||||||
|
|
||||||
class ExpenseSplitStatusEnum(enum.Enum):
|
class ExpenseSplitStatusEnum(enum.Enum):
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Numeric, DateTime, ForeignKey, Boolean, JSON, Enum as SQLEnum
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.base_class import Base
|
|
||||||
from app.models.enums import SplitTypeEnum, ExpenseOverallStatusEnum, ExpenseSplitStatusEnum
|
|
||||||
|
|
||||||
class RecurrencePattern(Base):
|
|
||||||
__tablename__ = "recurrence_patterns"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
type = Column(String, nullable=False) # 'daily', 'weekly', 'monthly', 'yearly'
|
|
||||||
interval = Column(Integer, nullable=False)
|
|
||||||
days_of_week = Column(JSON, nullable=True) # For weekly recurrence
|
|
||||||
end_date = Column(DateTime, nullable=True)
|
|
||||||
max_occurrences = Column(Integer, nullable=True)
|
|
||||||
created_at = Column(DateTime, nullable=False)
|
|
||||||
updated_at = Column(DateTime, nullable=False)
|
|
||||||
|
|
||||||
# Relationship
|
|
||||||
expense = relationship("Expense", back_populates="recurrence_pattern", uselist=False)
|
|
||||||
|
|
||||||
class Expense(Base):
|
|
||||||
__tablename__ = "expenses"
|
|
||||||
|
|
||||||
# ... existing columns ...
|
|
||||||
|
|
||||||
# New columns for recurring expenses
|
|
||||||
is_recurring = Column(Boolean, default=False, nullable=False)
|
|
||||||
next_occurrence = Column(DateTime, nullable=True)
|
|
||||||
last_occurrence = Column(DateTime, nullable=True)
|
|
||||||
recurrence_pattern_id = Column(Integer, ForeignKey("recurrence_patterns.id"), nullable=True)
|
|
||||||
|
|
||||||
# New relationship
|
|
||||||
recurrence_pattern = relationship("RecurrencePattern", back_populates="expense", uselist=False)
|
|
||||||
generated_expenses = relationship("Expense",
|
|
||||||
backref=relationship("parent_expense", remote_side=[id]),
|
|
||||||
foreign_keys="Expense.parent_expense_id")
|
|
||||||
parent_expense_id = Column(Integer, ForeignKey("expenses.id"), nullable=True)
|
|
||||||
|
|
||||||
# ... rest of existing code ...
|
|
Loading…
Reference in New Issue
Block a user