Alembic's offline mode generates SQL from SQLAlchemy migration operations, providing developers with a clear view of database schema changes without requiring an active database connection. However, some migration versions (specificallytags/1.4.0bbadea11beandd7999dfa4a) were performing database schema introspection, which fails in offline mode since it requires an actual database connection. This commit: - Adds offline mode support by detecting context.is_offline_mode() - Skips introspection steps when in offline mode - Adds warning messages in SQL output to inform users that assumptions were made - Prompts users to review the generated SQL for accuracy These changes ensure migrations work consistently in both online and offline modes. Close #19284.
| @@ -5,45 +5,61 @@ Revises: 33f5fac87f29 | |||
| Create Date: 2024-10-10 05:16:14.764268 | |||
| """ | |||
| from alembic import op | |||
| import models as models | |||
| import sqlalchemy as sa | |||
| from sqlalchemy.dialects import postgresql | |||
| from alembic import op, context | |||
| # revision identifiers, used by Alembic. | |||
| revision = 'bbadea11becb' | |||
| down_revision = 'd8e744d88ed6' | |||
| revision = "bbadea11becb" | |||
| down_revision = "d8e744d88ed6" | |||
| branch_labels = None | |||
| depends_on = None | |||
| def upgrade(): | |||
| def _has_name_or_size_column() -> bool: | |||
| # We cannot access the database in offline mode, so assume | |||
| # the "name" and "size" columns do not exist. | |||
| if context.is_offline_mode(): | |||
| # Log a warning message to inform the user that the database schema cannot be inspected | |||
| # in offline mode, and the generated SQL may not accurately reflect the actual execution. | |||
| op.execute( | |||
| "-- Executing in offline mode, assuming the name and size columns do not exist.\n" | |||
| "-- The generated SQL may differ from what will actually be executed.\n" | |||
| "-- Please review the migration script carefully!" | |||
| ) | |||
| return False | |||
| # Use SQLAlchemy inspector to get the columns of the 'tool_files' table | |||
| inspector = sa.inspect(conn) | |||
| columns = [col["name"] for col in inspector.get_columns("tool_files")] | |||
| # If 'name' or 'size' columns already exist, exit the upgrade function | |||
| if "name" in columns or "size" in columns: | |||
| return True | |||
| return False | |||
| # ### commands auto generated by Alembic - please adjust! ### | |||
| # Get the database connection | |||
| conn = op.get_bind() | |||
| # Use SQLAlchemy inspector to get the columns of the 'tool_files' table | |||
| inspector = sa.inspect(conn) | |||
| columns = [col['name'] for col in inspector.get_columns('tool_files')] | |||
| # If 'name' or 'size' columns already exist, exit the upgrade function | |||
| if 'name' in columns or 'size' in columns: | |||
| if _has_name_or_size_column(): | |||
| return | |||
| with op.batch_alter_table('tool_files', schema=None) as batch_op: | |||
| batch_op.add_column(sa.Column('name', sa.String(), nullable=True)) | |||
| batch_op.add_column(sa.Column('size', sa.Integer(), nullable=True)) | |||
| with op.batch_alter_table("tool_files", schema=None) as batch_op: | |||
| batch_op.add_column(sa.Column("name", sa.String(), nullable=True)) | |||
| batch_op.add_column(sa.Column("size", sa.Integer(), nullable=True)) | |||
| op.execute("UPDATE tool_files SET name = '' WHERE name IS NULL") | |||
| op.execute("UPDATE tool_files SET size = -1 WHERE size IS NULL") | |||
| with op.batch_alter_table('tool_files', schema=None) as batch_op: | |||
| batch_op.alter_column('name', existing_type=sa.String(), nullable=False) | |||
| batch_op.alter_column('size', existing_type=sa.Integer(), nullable=False) | |||
| with op.batch_alter_table("tool_files", schema=None) as batch_op: | |||
| batch_op.alter_column("name", existing_type=sa.String(), nullable=False) | |||
| batch_op.alter_column("size", existing_type=sa.Integer(), nullable=False) | |||
| # ### end Alembic commands ### | |||
| def downgrade(): | |||
| # ### commands auto generated by Alembic - please adjust! ### | |||
| with op.batch_alter_table('tool_files', schema=None) as batch_op: | |||
| batch_op.drop_column('size') | |||
| batch_op.drop_column('name') | |||
| with op.batch_alter_table("tool_files", schema=None) as batch_op: | |||
| batch_op.drop_column("size") | |||
| batch_op.drop_column("name") | |||
| # ### end Alembic commands ### | |||
| @@ -5,28 +5,38 @@ Revises: e1944c35e15e | |||
| Create Date: 2024-12-23 11:54:15.344543 | |||
| """ | |||
| from alembic import op | |||
| import models as models | |||
| import sqlalchemy as sa | |||
| from alembic import op, context | |||
| from sqlalchemy import inspect | |||
| # revision identifiers, used by Alembic. | |||
| revision = 'd7999dfa4aae' | |||
| down_revision = 'e1944c35e15e' | |||
| revision = "d7999dfa4aae" | |||
| down_revision = "e1944c35e15e" | |||
| branch_labels = None | |||
| depends_on = None | |||
| def upgrade(): | |||
| # Check if column exists before attempting to remove it | |||
| conn = op.get_bind() | |||
| inspector = inspect(conn) | |||
| has_column = 'retry_index' in [col['name'] for col in inspector.get_columns('workflow_node_executions')] | |||
| def _has_retry_index_column() -> bool: | |||
| if context.is_offline_mode(): | |||
| # Log a warning message to inform the user that the database schema cannot be inspected | |||
| # in offline mode, and the generated SQL may not accurately reflect the actual execution. | |||
| op.execute( | |||
| '-- Executing in offline mode: assuming the "retry_index" column does not exist.\n' | |||
| "-- The generated SQL may differ from what will actually be executed.\n" | |||
| "-- Please review the migration script carefully!" | |||
| ) | |||
| return False | |||
| conn = op.get_bind() | |||
| inspector = inspect(conn) | |||
| return "retry_index" in [col["name"] for col in inspector.get_columns("workflow_node_executions")] | |||
| has_column = _has_retry_index_column() | |||
| if has_column: | |||
| with op.batch_alter_table('workflow_node_executions', schema=None) as batch_op: | |||
| batch_op.drop_column('retry_index') | |||
| with op.batch_alter_table("workflow_node_executions", schema=None) as batch_op: | |||
| batch_op.drop_column("retry_index") | |||
| def downgrade(): | |||
| @@ -1,6 +1,6 @@ | |||
| import json | |||
| from datetime import datetime | |||
| from typing import Any, Optional, cast | |||
| from typing import Any, cast | |||
| import sqlalchemy as sa | |||
| from deprecated import deprecated | |||
| @@ -304,8 +304,11 @@ class DeprecatedPublishedAppTool(Base): | |||
| db.UniqueConstraint("app_id", "user_id", name="unique_published_app_tool"), | |||
| ) | |||
| id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) | |||
| # id of the app | |||
| app_id = db.Column(StringUUID, ForeignKey("apps.id"), nullable=False) | |||
| user_id: Mapped[str] = db.Column(StringUUID, nullable=False) | |||
| # who published this tool | |||
| description = db.Column(db.Text, nullable=False) | |||
| # llm_description of the tool, for LLM | |||
| @@ -325,34 +328,3 @@ class DeprecatedPublishedAppTool(Base): | |||
| @property | |||
| def description_i18n(self) -> I18nObject: | |||
| return I18nObject(**json.loads(self.description)) | |||
| id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) | |||
| user_id: Mapped[str] = db.Column(StringUUID, nullable=False) | |||
| tenant_id: Mapped[str] = db.Column(StringUUID, nullable=False) | |||
| conversation_id: Mapped[Optional[str]] = db.Column(StringUUID, nullable=True) | |||
| file_key: Mapped[str] = db.Column(db.String(255), nullable=False) | |||
| mimetype: Mapped[str] = db.Column(db.String(255), nullable=False) | |||
| original_url: Mapped[Optional[str]] = db.Column(db.String(2048), nullable=True) | |||
| name: Mapped[str] = mapped_column(default="") | |||
| size: Mapped[int] = mapped_column(default=-1) | |||
| def __init__( | |||
| self, | |||
| *, | |||
| user_id: str, | |||
| tenant_id: str, | |||
| conversation_id: Optional[str] = None, | |||
| file_key: str, | |||
| mimetype: str, | |||
| original_url: Optional[str] = None, | |||
| name: str, | |||
| size: int, | |||
| ): | |||
| self.user_id = user_id | |||
| self.tenant_id = tenant_id | |||
| self.conversation_id = conversation_id | |||
| self.file_key = file_key | |||
| self.mimetype = mimetype | |||
| self.original_url = original_url | |||
| self.name = name | |||
| self.size = size | |||