diff --git a/backend/open_webui/migrations/versions/018012973d35_add_indexes.py b/backend/open_webui/migrations/versions/018012973d35_add_indexes.py index b9f124f775..ca3313c7f0 100644 --- a/backend/open_webui/migrations/versions/018012973d35_add_indexes.py +++ b/backend/open_webui/migrations/versions/018012973d35_add_indexes.py @@ -16,18 +16,34 @@ depends_on = None def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + + def _idx_exists(table, idx_name): + return any(i['name'] == idx_name for i in inspector.get_indexes(table)) + # Chat table indexes - op.create_index('folder_id_idx', 'chat', ['folder_id']) - op.create_index('user_id_pinned_idx', 'chat', ['user_id', 'pinned']) - op.create_index('user_id_archived_idx', 'chat', ['user_id', 'archived']) - op.create_index('updated_at_user_id_idx', 'chat', ['updated_at', 'user_id']) - op.create_index('folder_id_user_id_idx', 'chat', ['folder_id', 'user_id']) + if not _idx_exists('chat', 'folder_id_idx'): + op.create_index('folder_id_idx', 'chat', ['folder_id']) + if not _idx_exists('chat', 'user_id_pinned_idx'): + op.create_index('user_id_pinned_idx', 'chat', ['user_id', 'pinned']) + if not _idx_exists('chat', 'user_id_archived_idx'): + op.create_index('user_id_archived_idx', 'chat', ['user_id', 'archived']) + if not _idx_exists('chat', 'updated_at_user_id_idx'): + op.create_index('updated_at_user_id_idx', 'chat', ['updated_at', 'user_id']) + if not _idx_exists('chat', 'folder_id_user_id_idx'): + op.create_index('folder_id_user_id_idx', 'chat', ['folder_id', 'user_id']) # Tag table index - op.create_index('user_id_idx', 'tag', ['user_id']) + if not _idx_exists('tag', 'user_id_idx'): + op.create_index('user_id_idx', 'tag', ['user_id']) - # Function table index - op.create_index('is_global_idx', 'function', ['is_global']) + # Function table index (only if is_global column exists — added by a later migration) + conn = op.get_bind() + inspector = sa.inspect(conn) + func_cols = {c['name'] for c in inspector.get_columns('function')} + if 'is_global' in func_cols and not _idx_exists('function', 'is_global_idx'): + op.create_index('is_global_idx', 'function', ['is_global']) def downgrade(): diff --git a/backend/open_webui/migrations/versions/374d2f66af06_add_prompt_history_table.py b/backend/open_webui/migrations/versions/374d2f66af06_add_prompt_history_table.py index 0a3521077e..69dd73c700 100644 --- a/backend/open_webui/migrations/versions/374d2f66af06_add_prompt_history_table.py +++ b/backend/open_webui/migrations/versions/374d2f66af06_add_prompt_history_table.py @@ -20,150 +20,167 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) - # Step 1: Read existing data from OLD table (schema likely command as PK) - # We use batch_alter previously, but we want to move to new table. - # We need to assume the OLD structure. + # If the final state already exists (prompt has 'id' PK + prompt_history exists), + # the migration completed successfully on a prior run — nothing to do. + if 'prompt_history' in existing_tables and 'prompt_new' not in existing_tables: + # prompt_history exists and prompt_new was already renamed → done + prompt_cols = {c['name'] for c in inspector.get_columns('prompt')} + if 'id' in prompt_cols and 'version_id' in prompt_cols: + return - old_prompt_table = sa.table( - 'prompt', - sa.column('command', sa.Text()), - sa.column('user_id', sa.Text()), - sa.column('title', sa.Text()), - sa.column('content', sa.Text()), - sa.column('timestamp', sa.BigInteger()), - sa.column('access_control', sa.JSON()), - ) - - # Check if table exists/read data - try: - existing_prompts = conn.execute( - sa.select( - old_prompt_table.c.command, - old_prompt_table.c.user_id, - old_prompt_table.c.title, - old_prompt_table.c.content, - old_prompt_table.c.timestamp, - old_prompt_table.c.access_control, + # Step 1: Read existing data from OLD table (schema: command as PK) + # Only read if the old-schema prompt table still exists (has 'command' but no 'version_id') + existing_prompts = [] + if 'prompt' in existing_tables and 'prompt_new' not in existing_tables: + prompt_cols = {c['name'] for c in inspector.get_columns('prompt')} + if 'command' in prompt_cols and 'version_id' not in prompt_cols: + old_prompt_table = sa.table( + 'prompt', + sa.column('command', sa.Text()), + sa.column('user_id', sa.Text()), + sa.column('title', sa.Text()), + sa.column('content', sa.Text()), + sa.column('timestamp', sa.BigInteger()), + sa.column('access_control', sa.JSON()), ) - ).fetchall() - except Exception: - # Fallback if table doesn't exist (new install) - existing_prompts = [] + try: + existing_prompts = conn.execute( + sa.select( + old_prompt_table.c.command, + old_prompt_table.c.user_id, + old_prompt_table.c.title, + old_prompt_table.c.content, + old_prompt_table.c.timestamp, + old_prompt_table.c.access_control, + ) + ).fetchall() + except Exception: + existing_prompts = [] - # Step 2: Create new prompt table with 'id' as PRIMARY KEY - op.create_table( - 'prompt_new', - sa.Column('id', sa.Text(), primary_key=True), - sa.Column('command', sa.String(), unique=True, index=True), - sa.Column('user_id', sa.String(), nullable=False), - sa.Column('name', sa.Text(), nullable=False), - sa.Column('content', sa.Text(), nullable=False), - sa.Column('data', sa.JSON(), nullable=True), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('access_control', sa.JSON(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'), - sa.Column('version_id', sa.Text(), nullable=True), - sa.Column('tags', sa.JSON(), nullable=True), - sa.Column('created_at', sa.BigInteger(), nullable=False), - sa.Column('updated_at', sa.BigInteger(), nullable=False), - ) - - # Step 3: Create prompt_history table - op.create_table( - 'prompt_history', - sa.Column('id', sa.Text(), primary_key=True), - sa.Column('prompt_id', sa.Text(), nullable=False, index=True), - sa.Column('parent_id', sa.Text(), nullable=True), - sa.Column('snapshot', sa.JSON(), nullable=False), - sa.Column('user_id', sa.Text(), nullable=False), - sa.Column('commit_message', sa.Text(), nullable=True), - sa.Column('created_at', sa.BigInteger(), nullable=False), - ) - - # Step 4: Migrate data - prompt_new_table = sa.table( - 'prompt_new', - sa.column('id', sa.Text()), - sa.column('command', sa.String()), - sa.column('user_id', sa.String()), - sa.column('name', sa.Text()), - sa.column('content', sa.Text()), - sa.column('data', sa.JSON()), - sa.column('meta', sa.JSON()), - sa.column('access_control', sa.JSON()), - sa.column('is_active', sa.Boolean()), - sa.column('version_id', sa.Text()), - sa.column('tags', sa.JSON()), - sa.column('created_at', sa.BigInteger()), - sa.column('updated_at', sa.BigInteger()), - ) - - prompt_history_table = sa.table( - 'prompt_history', - sa.column('id', sa.Text()), - sa.column('prompt_id', sa.Text()), - sa.column('parent_id', sa.Text()), - sa.column('snapshot', sa.JSON()), - sa.column('user_id', sa.Text()), - sa.column('commit_message', sa.Text()), - sa.column('created_at', sa.BigInteger()), - ) - - for row in existing_prompts: - command = row[0] - user_id = row[1] - title = row[2] - content = row[3] - timestamp = row[4] - access_control = row[5] - - new_uuid = str(uuid.uuid4()) - history_uuid = str(uuid.uuid4()) - clean_command = command[1:] if command and command.startswith('/') else command - - # Insert into prompt_new - conn.execute( - sa.insert(prompt_new_table).values( - id=new_uuid, - command=clean_command, - user_id=user_id, - name=title, - content=content, - data={}, - meta={}, - access_control=access_control, - is_active=True, - version_id=history_uuid, - tags=[], - created_at=timestamp, - updated_at=timestamp, - ) + # Step 2: Create new prompt table with 'id' as PRIMARY KEY (if not already created) + if 'prompt_new' not in existing_tables: + op.create_table( + 'prompt_new', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('command', sa.String(), unique=True, index=True), + sa.Column('user_id', sa.String(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('access_control', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'), + sa.Column('version_id', sa.Text(), nullable=True), + sa.Column('tags', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), ) - # Create initial history entry - conn.execute( - sa.insert(prompt_history_table).values( - id=history_uuid, - prompt_id=new_uuid, - parent_id=None, - snapshot={ - 'name': title, - 'content': content, - 'command': clean_command, - 'data': {}, - 'meta': {}, - 'access_control': access_control, - }, - user_id=user_id, - commit_message=None, - created_at=timestamp, - ) + # Step 3: Create prompt_history table (if not already created) + if 'prompt_history' not in existing_tables: + op.create_table( + 'prompt_history', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('prompt_id', sa.Text(), nullable=False, index=True), + sa.Column('parent_id', sa.Text(), nullable=True), + sa.Column('snapshot', sa.JSON(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('commit_message', sa.Text(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), ) - # Step 5: Replace old table with new one - op.drop_table('prompt') - op.rename_table('prompt_new', 'prompt') + # Step 4: Migrate data (only if we have old data to migrate) + if existing_prompts: + prompt_new_table = sa.table( + 'prompt_new', + sa.column('id', sa.Text()), + sa.column('command', sa.String()), + sa.column('user_id', sa.String()), + sa.column('name', sa.Text()), + sa.column('content', sa.Text()), + sa.column('data', sa.JSON()), + sa.column('meta', sa.JSON()), + sa.column('access_control', sa.JSON()), + sa.column('is_active', sa.Boolean()), + sa.column('version_id', sa.Text()), + sa.column('tags', sa.JSON()), + sa.column('created_at', sa.BigInteger()), + sa.column('updated_at', sa.BigInteger()), + ) + + prompt_history_table = sa.table( + 'prompt_history', + sa.column('id', sa.Text()), + sa.column('prompt_id', sa.Text()), + sa.column('parent_id', sa.Text()), + sa.column('snapshot', sa.JSON()), + sa.column('user_id', sa.Text()), + sa.column('commit_message', sa.Text()), + sa.column('created_at', sa.BigInteger()), + ) + + for row in existing_prompts: + command = row[0] + user_id = row[1] + title = row[2] + content = row[3] + timestamp = row[4] + access_control = row[5] + + new_uuid = str(uuid.uuid4()) + history_uuid = str(uuid.uuid4()) + clean_command = command[1:] if command and command.startswith('/') else command + + # Insert into prompt_new + conn.execute( + sa.insert(prompt_new_table).values( + id=new_uuid, + command=clean_command, + user_id=user_id, + name=title, + content=content, + data={}, + meta={}, + access_control=access_control, + is_active=True, + version_id=history_uuid, + tags=[], + created_at=timestamp, + updated_at=timestamp, + ) + ) + + # Create initial history entry + conn.execute( + sa.insert(prompt_history_table).values( + id=history_uuid, + prompt_id=new_uuid, + parent_id=None, + snapshot={ + 'name': title, + 'content': content, + 'command': clean_command, + 'data': {}, + 'meta': {}, + 'access_control': access_control, + }, + user_id=user_id, + commit_message=None, + created_at=timestamp, + ) + ) + + # Step 5: Replace old table with new one (only if prompt_new exists) + # Re-check tables after potential creation above + inspector.clear_cache() + current_tables = set(inspector.get_table_names()) + if 'prompt_new' in current_tables: + if 'prompt' in current_tables: + op.drop_table('prompt') + op.rename_table('prompt_new', 'prompt') def downgrade() -> None: diff --git a/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py b/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py index a609426ce8..ccb12515f1 100644 --- a/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py +++ b/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py @@ -158,10 +158,13 @@ def upgrade() -> None: if col_name not in user_columns: op.add_column('user', sa.Column(col_name, col_type, nullable=True)) - # Convert info (TEXT/JSONField) → JSON - _convert_column_to_json('user', 'info') - # Convert settings (TEXT/JSONField) → JSON - _convert_column_to_json('user', 'settings') + # Convert info (TEXT/JSONField) → JSON (skip if already JSON) + user_col_types = {c['name']: c['type'] for c in inspector.get_columns('user')} + if isinstance(user_col_types.get('info'), sa.Text): + _convert_column_to_json('user', 'info') + # Convert settings (TEXT/JSONField) → JSON (skip if already JSON) + if isinstance(user_col_types.get('settings'), sa.Text): + _convert_column_to_json('user', 'settings') # ── Create api_key table (idempotent) ───────────────────────────── if 'api_key' not in existing_tables: