From 81f611fb73c726cfcfc20ee57a926a543f07e95f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 14 May 2026 03:06:37 +0900 Subject: [PATCH] refac --- .../56359461a091_add_calendar_tables.py | 121 +++++++++++------- ...d5bedd151_add_tasks_and_summary_to_chat.py | 10 +- .../b7c8d9e0f1a2_add_last_read_at_to_chat.py | 11 +- .../c1d2e3f4a5b6_add_shared_chat_table.py | 91 ++++++++----- .../d4e5f6a7b8c9_add_automation_tables.py | 77 ++++++----- .../e1f2a3b4c5d6_add_is_pinned_to_note.py | 7 +- 6 files changed, 200 insertions(+), 117 deletions(-) diff --git a/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py b/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py index 648d11b880..7a61e3a0af 100644 --- a/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py +++ b/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py @@ -18,58 +18,81 @@ branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None +def _index_exists(inspector, index_name, table_name): + """Check if an index already exists on the given table.""" + indexes = inspector.get_indexes(table_name) + return any(idx['name'] == index_name for idx in indexes) + + def upgrade() -> None: - op.create_table( - 'calendar', - sa.Column('id', sa.Text(), nullable=False), - sa.Column('user_id', sa.Text(), nullable=False), - sa.Column('name', sa.Text(), nullable=False), - sa.Column('color', sa.Text(), nullable=True), - sa.Column('is_default', sa.Boolean(), nullable=False), - sa.Column('data', sa.JSON(), nullable=True), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('created_at', sa.BigInteger(), nullable=False), - sa.Column('updated_at', sa.BigInteger(), nullable=False), - sa.PrimaryKeyConstraint('id'), - ) - op.create_index('ix_calendar_user', 'calendar', ['user_id'], unique=False) + conn = op.get_bind() + inspector = sa.inspect(conn) + tables = inspector.get_table_names() - op.create_table( - 'calendar_event', - sa.Column('id', sa.Text(), nullable=False), - sa.Column('calendar_id', sa.Text(), nullable=False), - sa.Column('user_id', sa.Text(), nullable=False), - sa.Column('title', sa.Text(), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('start_at', sa.BigInteger(), nullable=False), - sa.Column('end_at', sa.BigInteger(), nullable=True), - sa.Column('all_day', sa.Boolean(), nullable=False), - sa.Column('rrule', sa.Text(), nullable=True), - sa.Column('color', sa.Text(), nullable=True), - sa.Column('location', sa.Text(), nullable=True), - sa.Column('data', sa.JSON(), nullable=True), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('is_cancelled', sa.Boolean(), nullable=False), - sa.Column('created_at', sa.BigInteger(), nullable=False), - sa.Column('updated_at', sa.BigInteger(), nullable=False), - sa.PrimaryKeyConstraint('id'), - ) - op.create_index('ix_calendar_event_calendar', 'calendar_event', ['calendar_id', 'start_at'], unique=False) - op.create_index('ix_calendar_event_user_date', 'calendar_event', ['user_id', 'start_at'], unique=False) + if 'calendar' not in tables: + op.create_table( + 'calendar', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('color', sa.Text(), nullable=True), + sa.Column('is_default', sa.Boolean(), nullable=False), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), + ) - op.create_table( - 'calendar_event_attendee', - sa.Column('id', sa.Text(), nullable=False), - sa.Column('event_id', sa.Text(), nullable=False), - sa.Column('user_id', sa.Text(), nullable=False), - sa.Column('status', sa.Text(), nullable=False), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('created_at', sa.BigInteger(), nullable=False), - sa.Column('updated_at', sa.BigInteger(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('event_id', 'user_id', name='uq_event_attendee'), - ) - op.create_index('ix_calendar_event_attendee_user', 'calendar_event_attendee', ['user_id', 'status'], unique=False) + if 'calendar' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_calendar_user', 'calendar'): + op.create_index('ix_calendar_user', 'calendar', ['user_id'], unique=False) + + if 'calendar_event' not in tables: + op.create_table( + 'calendar_event', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('calendar_id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('title', sa.Text(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('start_at', sa.BigInteger(), nullable=False), + sa.Column('end_at', sa.BigInteger(), nullable=True), + sa.Column('all_day', sa.Boolean(), nullable=False), + sa.Column('rrule', sa.Text(), nullable=True), + sa.Column('color', sa.Text(), nullable=True), + sa.Column('location', sa.Text(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('is_cancelled', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), + ) + + if 'calendar_event' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_calendar_event_calendar', 'calendar_event'): + op.create_index('ix_calendar_event_calendar', 'calendar_event', ['calendar_id', 'start_at'], unique=False) + if not _index_exists(inspector, 'ix_calendar_event_user_date', 'calendar_event'): + op.create_index('ix_calendar_event_user_date', 'calendar_event', ['user_id', 'start_at'], unique=False) + + if 'calendar_event_attendee' not in tables: + op.create_table( + 'calendar_event_attendee', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('event_id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('status', sa.Text(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('event_id', 'user_id', name='uq_event_attendee'), + ) + + if 'calendar_event_attendee' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_calendar_event_attendee_user', 'calendar_event_attendee'): + op.create_index('ix_calendar_event_attendee_user', 'calendar_event_attendee', ['user_id', 'status'], unique=False) def downgrade() -> None: diff --git a/backend/open_webui/migrations/versions/a3dd5bedd151_add_tasks_and_summary_to_chat.py b/backend/open_webui/migrations/versions/a3dd5bedd151_add_tasks_and_summary_to_chat.py index 088277cd1a..5ccff52312 100644 --- a/backend/open_webui/migrations/versions/a3dd5bedd151_add_tasks_and_summary_to_chat.py +++ b/backend/open_webui/migrations/versions/a3dd5bedd151_add_tasks_and_summary_to_chat.py @@ -19,8 +19,14 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - op.add_column('chat', sa.Column('tasks', sa.JSON(), nullable=True)) - op.add_column('chat', sa.Column('summary', sa.Text(), nullable=True)) + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('chat')] + + if 'tasks' not in columns: + op.add_column('chat', sa.Column('tasks', sa.JSON(), nullable=True)) + if 'summary' not in columns: + op.add_column('chat', sa.Column('summary', sa.Text(), nullable=True)) def downgrade() -> None: diff --git a/backend/open_webui/migrations/versions/b7c8d9e0f1a2_add_last_read_at_to_chat.py b/backend/open_webui/migrations/versions/b7c8d9e0f1a2_add_last_read_at_to_chat.py index bacd4cf90f..fd298b7929 100644 --- a/backend/open_webui/migrations/versions/b7c8d9e0f1a2_add_last_read_at_to_chat.py +++ b/backend/open_webui/migrations/versions/b7c8d9e0f1a2_add_last_read_at_to_chat.py @@ -17,9 +17,14 @@ depends_on = None def upgrade(): - op.add_column('chat', sa.Column('last_read_at', sa.BigInteger(), nullable=True)) - # Set existing chats to be marked as read - op.execute('UPDATE chat SET last_read_at = updated_at') + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('chat')] + + if 'last_read_at' not in columns: + op.add_column('chat', sa.Column('last_read_at', sa.BigInteger(), nullable=True)) + # Set existing chats to be marked as read + op.execute('UPDATE chat SET last_read_at = updated_at') def downgrade(): diff --git a/backend/open_webui/migrations/versions/c1d2e3f4a5b6_add_shared_chat_table.py b/backend/open_webui/migrations/versions/c1d2e3f4a5b6_add_shared_chat_table.py index 8f4fec8deb..b70022e168 100644 --- a/backend/open_webui/migrations/versions/c1d2e3f4a5b6_add_shared_chat_table.py +++ b/backend/open_webui/migrations/versions/c1d2e3f4a5b6_add_shared_chat_table.py @@ -61,18 +61,21 @@ access_grant_t = sa.table( def upgrade(): conn = op.get_bind() + inspector = sa.inspect(conn) + tables = inspector.get_table_names() - # 1. Create shared_chat table - op.create_table( - 'shared_chat', - sa.Column('id', sa.Text(), primary_key=True), - sa.Column('chat_id', sa.Text(), sa.ForeignKey('chat.id', ondelete='CASCADE'), nullable=False), - sa.Column('user_id', sa.Text(), nullable=False), - sa.Column('title', sa.Text(), nullable=True), - sa.Column('chat', sa.JSON(), nullable=True), - sa.Column('created_at', sa.BigInteger(), nullable=True), - sa.Column('updated_at', sa.BigInteger(), nullable=True), - ) + # 1. Create shared_chat table (idempotent) + if 'shared_chat' not in tables: + op.create_table( + 'shared_chat', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('chat_id', sa.Text(), sa.ForeignKey('chat.id', ondelete='CASCADE'), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('title', sa.Text(), nullable=True), + sa.Column('chat', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + ) # 2. Migrate existing shared-* rows shared_rows = conn.execute( @@ -96,31 +99,53 @@ def upgrade(): if not original: continue - # Insert snapshot into shared_chat - conn.execute( - shared_chat_t.insert().values( - id=share_token, - chat_id=original_chat_id, - user_id=original.user_id, - title=row.title, - chat=row.chat, - created_at=row.created_at, - updated_at=row.updated_at, + # Check if shared_chat record already exists (idempotent) + existing_shared = conn.execute( + sa.select(shared_chat_t.c.id).where( + shared_chat_t.c.id == share_token ) - ) + ).fetchone() - # Create user:*:read grant for backward compat - conn.execute( - access_grant_t.insert().values( - id=str(uuid.uuid4()), - resource_type='shared_chat', - resource_id=original_chat_id, - principal_type='user', - principal_id='*', - permission='read', - created_at=row.created_at or int(time.time()), + if not existing_shared: + # Insert snapshot into shared_chat + conn.execute( + shared_chat_t.insert().values( + id=share_token, + chat_id=original_chat_id, + user_id=original.user_id, + title=row.title, + chat=row.chat, + created_at=row.created_at, + updated_at=row.updated_at, + ) + ) + + # Check if access_grant record already exists (idempotent) + existing_grant = conn.execute( + sa.select(access_grant_t.c.id).where( + sa.and_( + access_grant_t.c.resource_type == 'shared_chat', + access_grant_t.c.resource_id == original_chat_id, + access_grant_t.c.principal_type == 'user', + access_grant_t.c.principal_id == '*', + access_grant_t.c.permission == 'read', + ) + ) + ).fetchone() + + if not existing_grant: + # Create user:*:read grant for backward compat + conn.execute( + access_grant_t.insert().values( + id=str(uuid.uuid4()), + resource_type='shared_chat', + resource_id=original_chat_id, + principal_type='user', + principal_id='*', + permission='read', + created_at=row.created_at or int(time.time()), + ) ) - ) # 3. Clean up old phantom rows conn.execute( diff --git a/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py b/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py index c6f3ccb593..30fc2bb871 100644 --- a/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py +++ b/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py @@ -16,36 +16,55 @@ branch_labels = None depends_on = None -def upgrade(): - op.create_table( - 'automation', - sa.Column('id', sa.Text(), primary_key=True), - sa.Column('user_id', sa.Text(), nullable=False), - sa.Column('name', sa.Text(), nullable=False), - sa.Column('data', sa.JSON(), nullable=False), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=False, default=True), - sa.Column('last_run_at', sa.BigInteger(), nullable=True), - sa.Column('next_run_at', sa.BigInteger(), nullable=True), - sa.Column('created_at', sa.BigInteger(), nullable=False), - sa.Column('updated_at', sa.BigInteger(), nullable=False), - ) - op.create_index('ix_automation_next_run', 'automation', ['next_run_at']) +def _index_exists(inspector, index_name, table_name): + """Check if an index already exists on the given table (works for both SQLite and PostgreSQL).""" + indexes = inspector.get_indexes(table_name) + return any(idx['name'] == index_name for idx in indexes) - op.create_table( - 'automation_run', - sa.Column('id', sa.Text(), primary_key=True), - sa.Column('automation_id', sa.Text(), nullable=False), - sa.Column('chat_id', sa.Text(), nullable=True), - sa.Column('status', sa.Text(), nullable=False), - sa.Column('error', sa.Text(), nullable=True), - sa.Column('created_at', sa.BigInteger(), nullable=False), - ) - op.create_index( - 'ix_automation_run_automation_id', - 'automation_run', - ['automation_id'], - ) + +def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + tables = inspector.get_table_names() + + if 'automation' not in tables: + op.create_table( + 'automation', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('data', sa.JSON(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, default=True), + sa.Column('last_run_at', sa.BigInteger(), nullable=True), + sa.Column('next_run_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + ) + + # Re-check tables in case we just created it + if 'automation' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_automation_next_run', 'automation'): + op.create_index('ix_automation_next_run', 'automation', ['next_run_at']) + + if 'automation_run' not in tables: + op.create_table( + 'automation_run', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('automation_id', sa.Text(), nullable=False), + sa.Column('chat_id', sa.Text(), nullable=True), + sa.Column('status', sa.Text(), nullable=False), + sa.Column('error', sa.Text(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + ) + + if 'automation_run' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_automation_run_automation_id', 'automation_run'): + op.create_index( + 'ix_automation_run_automation_id', + 'automation_run', + ['automation_id'], + ) def downgrade(): diff --git a/backend/open_webui/migrations/versions/e1f2a3b4c5d6_add_is_pinned_to_note.py b/backend/open_webui/migrations/versions/e1f2a3b4c5d6_add_is_pinned_to_note.py index e086a333da..7232b11b83 100644 --- a/backend/open_webui/migrations/versions/e1f2a3b4c5d6_add_is_pinned_to_note.py +++ b/backend/open_webui/migrations/versions/e1f2a3b4c5d6_add_is_pinned_to_note.py @@ -16,7 +16,12 @@ depends_on = None def upgrade(): - op.add_column('note', sa.Column('is_pinned', sa.Boolean(), nullable=True)) + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('note')] + + if 'is_pinned' not in columns: + op.add_column('note', sa.Column('is_pinned', sa.Boolean(), nullable=True)) def downgrade():