From fb5f624e89f8c52787817dde408e01063a250f46 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 16 Dec 2025 11:05:22 +0800 Subject: [PATCH] [add] migrations script --- .../versions/2a46f1e9a590_202512151530.py | 132 +++++++++++ .../versions/64ddbf3c3bcc_202512151837.py | 212 ++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 api/migrations/versions/2a46f1e9a590_202512151530.py create mode 100644 api/migrations/versions/64ddbf3c3bcc_202512151837.py diff --git a/api/migrations/versions/2a46f1e9a590_202512151530.py b/api/migrations/versions/2a46f1e9a590_202512151530.py new file mode 100644 index 00000000..6c68b25b --- /dev/null +++ b/api/migrations/versions/2a46f1e9a590_202512151530.py @@ -0,0 +1,132 @@ +"""202512151530 + +Revision ID: 2a46f1e9a590 +Revises: 94a98e279951 +Create Date: 2025-12-15 15:27:33.975790 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import inspect +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '2a46f1e9a590' +down_revision: Union[str, None] = '94a98e279951' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def table_exists(table_name: str) -> bool: + """检查表是否存在""" + bind = op.get_bind() + inspector = inspect(bind) + return table_name in inspector.get_table_names() + + +def column_exists(table_name: str, column_name: str) -> bool: + """检查列是否存在""" + bind = op.get_bind() + inspector = inspect(bind) + if not table_exists(table_name): + return False + columns = [col['name'] for col in inspector.get_columns(table_name)] + return column_name in columns + + +def index_exists(table_name: str, index_name: str) -> bool: + """检查索引是否存在""" + bind = op.get_bind() + inspector = inspect(bind) + if not table_exists(table_name): + return False + indexes = [idx['name'] for idx in inspector.get_indexes(table_name)] + return index_name in indexes + + +def constraint_exists(table_name: str, constraint_name: str) -> bool: + """检查约束是否存在(外键、唯一约束等)""" + bind = op.get_bind() + inspector = inspect(bind) + if not table_exists(table_name): + return False + + # 检查外键约束 + foreign_keys = [fk['name'] for fk in inspector.get_foreign_keys(table_name) if fk['name']] + if constraint_name in foreign_keys: + return True + + # 检查唯一约束 + unique_constraints = [uc['name'] for uc in inspector.get_unique_constraints(table_name) if uc['name']] + if constraint_name in unique_constraints: + return True + + # 检查检查约束 + check_constraints = [cc['name'] for cc in inspector.get_check_constraints(table_name) if cc['name']] + if constraint_name in check_constraints: + return True + + return False + + +def trigger_exists(trigger_name: str) -> bool: + """检查触发器是否存在(PostgreSQL)""" + bind = op.get_bind() + result = bind.execute(sa.text( + "SELECT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = :trigger_name)" + ), {"trigger_name": trigger_name}) + return result.scalar() + + +def sequence_exists(sequence_name: str) -> bool: + """检查序列是否存在(PostgreSQL)""" + bind = op.get_bind() + result = bind.execute(sa.text( + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relkind = 'S' AND relname = :sequence_name)" + ), {"sequence_name": sequence_name}) + return result.scalar() + + +def enum_exists(enum_name: str) -> bool: + """检查枚举类型是否存在(PostgreSQL)""" + bind = op.get_bind() + result = bind.execute(sa.text( + "SELECT EXISTS (SELECT 1 FROM pg_type WHERE typname = :enum_name)" + ), {"enum_name": enum_name}) + return result.scalar() + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('api_keys', schema=None) as batch_op: + batch_op.add_column(sa.Column('api_key', sa.String(length=255), nullable=False, comment='API Key 明文')) + batch_op.alter_column('scopes', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + existing_comment='权限范围列表') + batch_op.drop_index(batch_op.f('ix_api_keys_key_hash')) + batch_op.create_index(batch_op.f('ix_api_keys_api_key'), ['api_key'], unique=True) + batch_op.drop_column('key_hash') + batch_op.drop_column('resource_type') + batch_op.drop_column('key_prefix') + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('api_keys', schema=None) as batch_op: + batch_op.add_column(sa.Column('key_prefix', sa.VARCHAR(length=20), autoincrement=False, nullable=False, comment='Key 前缀')) + batch_op.add_column(sa.Column('resource_type', sa.VARCHAR(length=50), autoincrement=False, nullable=True, comment='资源类型')) + batch_op.add_column(sa.Column('key_hash', sa.VARCHAR(length=255), autoincrement=False, nullable=False, comment='Key 哈希值')) + batch_op.drop_index(batch_op.f('ix_api_keys_api_key')) + batch_op.create_index(batch_op.f('ix_api_keys_key_hash'), ['key_hash'], unique=True) + batch_op.alter_column('scopes', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + nullable=False, + existing_comment='权限范围列表') + batch_op.drop_column('api_key') + + # ### end Alembic commands ### diff --git a/api/migrations/versions/64ddbf3c3bcc_202512151837.py b/api/migrations/versions/64ddbf3c3bcc_202512151837.py new file mode 100644 index 00000000..ca957921 --- /dev/null +++ b/api/migrations/versions/64ddbf3c3bcc_202512151837.py @@ -0,0 +1,212 @@ +"""202512151837 + +Revision ID: 64ddbf3c3bcc +Revises: 2a46f1e9a590 +Create Date: 2025-12-15 18:37:42.649720 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import inspect +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '64ddbf3c3bcc' +down_revision: Union[str, None] = '2a46f1e9a590' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def table_exists(table_name: str) -> bool: + """检查表是否存在""" + bind = op.get_bind() + inspector = inspect(bind) + return table_name in inspector.get_table_names() + + +def column_exists(table_name: str, column_name: str) -> bool: + """检查列是否存在""" + bind = op.get_bind() + inspector = inspect(bind) + if not table_exists(table_name): + return False + columns = [col['name'] for col in inspector.get_columns(table_name)] + return column_name in columns + + +def index_exists(table_name: str, index_name: str) -> bool: + """检查索引是否存在""" + bind = op.get_bind() + inspector = inspect(bind) + if not table_exists(table_name): + return False + indexes = [idx['name'] for idx in inspector.get_indexes(table_name)] + return index_name in indexes + + +def constraint_exists(table_name: str, constraint_name: str) -> bool: + """检查约束是否存在(外键、唯一约束等)""" + bind = op.get_bind() + inspector = inspect(bind) + if not table_exists(table_name): + return False + + # 检查外键约束 + foreign_keys = [fk['name'] for fk in inspector.get_foreign_keys(table_name) if fk['name']] + if constraint_name in foreign_keys: + return True + + # 检查唯一约束 + unique_constraints = [uc['name'] for uc in inspector.get_unique_constraints(table_name) if uc['name']] + if constraint_name in unique_constraints: + return True + + # 检查检查约束 + check_constraints = [cc['name'] for cc in inspector.get_check_constraints(table_name) if cc['name']] + if constraint_name in check_constraints: + return True + + return False + + +def trigger_exists(trigger_name: str) -> bool: + """检查触发器是否存在(PostgreSQL)""" + bind = op.get_bind() + result = bind.execute(sa.text( + "SELECT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = :trigger_name)" + ), {"trigger_name": trigger_name}) + return result.scalar() + + +def sequence_exists(sequence_name: str) -> bool: + """检查序列是否存在(PostgreSQL)""" + bind = op.get_bind() + result = bind.execute(sa.text( + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relkind = 'S' AND relname = :sequence_name)" + ), {"sequence_name": sequence_name}) + return result.scalar() + + +def enum_exists(enum_name: str) -> bool: + """检查枚举类型是否存在(PostgreSQL)""" + bind = op.get_bind() + result = bind.execute(sa.text( + "SELECT EXISTS (SELECT 1 FROM pg_type WHERE typname = :enum_name)" + ), {"enum_name": enum_name}) + return result.scalar() + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('workflow_configs', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('app_id', sa.UUID(), nullable=False), + sa.Column('nodes', postgresql.JSONB(astext_type=sa.Text()), nullable=False), + sa.Column('edges', postgresql.JSONB(astext_type=sa.Text()), nullable=False), + sa.Column('variables', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('execution_config', postgresql.JSONB(astext_type=sa.Text()), nullable=False), + sa.Column('triggers', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['app_id'], ['apps.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('workflow_configs', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_workflow_configs_app_id'), ['app_id'], unique=True) + batch_op.create_index(batch_op.f('ix_workflow_configs_id'), ['id'], unique=False) + + op.create_table('workflow_executions', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('workflow_config_id', sa.UUID(), nullable=False), + sa.Column('app_id', sa.UUID(), nullable=False), + sa.Column('conversation_id', sa.UUID(), nullable=True), + sa.Column('execution_id', sa.String(length=100), nullable=False), + sa.Column('trigger_type', sa.String(length=20), nullable=False), + sa.Column('triggered_by', sa.UUID(), nullable=True), + sa.Column('input_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('output_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('context', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('error_node_id', sa.String(length=100), nullable=True), + sa.Column('started_at', sa.DateTime(), nullable=False), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.Column('elapsed_time', sa.Float(), nullable=True), + sa.Column('token_usage', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('meta_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['app_id'], ['apps.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['conversation_id'], ['conversations.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['triggered_by'], ['users.id'], ), + sa.ForeignKeyConstraint(['workflow_config_id'], ['workflow_configs.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('workflow_executions', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_workflow_executions_app_id'), ['app_id'], unique=False) + batch_op.create_index(batch_op.f('ix_workflow_executions_conversation_id'), ['conversation_id'], unique=False) + batch_op.create_index(batch_op.f('ix_workflow_executions_execution_id'), ['execution_id'], unique=True) + batch_op.create_index(batch_op.f('ix_workflow_executions_id'), ['id'], unique=False) + batch_op.create_index(batch_op.f('ix_workflow_executions_started_at'), ['started_at'], unique=False) + batch_op.create_index(batch_op.f('ix_workflow_executions_status'), ['status'], unique=False) + batch_op.create_index(batch_op.f('ix_workflow_executions_workflow_config_id'), ['workflow_config_id'], unique=False) + + op.create_table('workflow_node_executions', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('execution_id', sa.UUID(), nullable=False), + sa.Column('node_id', sa.String(length=100), nullable=False), + sa.Column('node_type', sa.String(length=20), nullable=False), + sa.Column('node_name', sa.String(length=100), nullable=True), + sa.Column('execution_order', sa.Integer(), nullable=False), + sa.Column('retry_count', sa.Integer(), nullable=False), + sa.Column('input_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('output_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('started_at', sa.DateTime(), nullable=False), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.Column('elapsed_time', sa.Float(), nullable=True), + sa.Column('token_usage', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('cache_hit', sa.Boolean(), nullable=True), + sa.Column('cache_key', sa.String(length=255), nullable=True), + sa.Column('meta_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['execution_id'], ['workflow_executions.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('workflow_node_executions', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_workflow_node_executions_execution_id'), ['execution_id'], unique=False) + batch_op.create_index(batch_op.f('ix_workflow_node_executions_id'), ['id'], unique=False) + batch_op.create_index(batch_op.f('ix_workflow_node_executions_node_id'), ['node_id'], unique=False) + batch_op.create_index(batch_op.f('ix_workflow_node_executions_status'), ['status'], unique=False) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('workflow_node_executions', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_workflow_node_executions_status')) + batch_op.drop_index(batch_op.f('ix_workflow_node_executions_node_id')) + batch_op.drop_index(batch_op.f('ix_workflow_node_executions_id')) + batch_op.drop_index(batch_op.f('ix_workflow_node_executions_execution_id')) + + op.drop_table('workflow_node_executions') + with op.batch_alter_table('workflow_executions', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_workflow_executions_workflow_config_id')) + batch_op.drop_index(batch_op.f('ix_workflow_executions_status')) + batch_op.drop_index(batch_op.f('ix_workflow_executions_started_at')) + batch_op.drop_index(batch_op.f('ix_workflow_executions_id')) + batch_op.drop_index(batch_op.f('ix_workflow_executions_execution_id')) + batch_op.drop_index(batch_op.f('ix_workflow_executions_conversation_id')) + batch_op.drop_index(batch_op.f('ix_workflow_executions_app_id')) + + op.drop_table('workflow_executions') + with op.batch_alter_table('workflow_configs', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_workflow_configs_id')) + batch_op.drop_index(batch_op.f('ix_workflow_configs_app_id')) + + op.drop_table('workflow_configs') + # ### end Alembic commands ###