Ver código fonte

清理外键使用

xueyiming 4 dias atrás
pai
commit
b2a135d18b

+ 0 - 3
alembic/versions/20260729_03_schema_baseline.py

@@ -72,7 +72,6 @@ def upgrade() -> None:
         sa.Column(
             "run_id",
             sa.String(36),
-            sa.ForeignKey("pipeline_run.run_id", ondelete="CASCADE"),
             nullable=False,
         ),
         sa.Column("step_key", sa.String(64), nullable=False),
@@ -164,13 +163,11 @@ def upgrade() -> None:
         sa.Column(
             "run_id",
             sa.String(36),
-            sa.ForeignKey("pipeline_run.run_id", ondelete="CASCADE"),
             nullable=False,
         ),
         sa.Column(
             "step_run_id",
             sa.String(36),
-            sa.ForeignKey("pipeline_step_run.step_run_id", ondelete="CASCADE"),
             nullable=False,
         ),
         sa.Column("effect_type", sa.String(64), nullable=False),

+ 0 - 1
alembic/versions/20260730_04_add_local_auth.py

@@ -53,7 +53,6 @@ def upgrade() -> None:
         sa.Column("ip_address", sa.String(length=64), nullable=True),
         sa.Column("user_agent", sa.String(length=512), nullable=True),
         sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
-        sa.ForeignKeyConstraint(["user_id"], ["auth_user.id"], ondelete="CASCADE"),
         sa.PrimaryKeyConstraint("id"),
         sa.UniqueConstraint("token_hash", name="uq_auth_session_token_hash"),
         mysql_charset="utf8mb4",

+ 0 - 1
alembic/versions/20260731_08a_create_video_discovery_tables.py

@@ -129,7 +129,6 @@ def _create_candidate() -> None:
         sa.Column("aigc_plan_label", sa.String(64), nullable=True),
         sa.Column("create_time", sa.DateTime(), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
         sa.Column("update_time", sa.DateTime(), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
-        sa.ForeignKeyConstraint(["search_id"], ["video_discovery_search.id"], name="fk_video_discovery_candidate_search", ondelete="RESTRICT"),
         sa.PrimaryKeyConstraint("id"),
         mysql_charset="utf8mb4",
         mysql_collate="utf8mb4_unicode_ci",

+ 0 - 1
alembic/versions/20260811_12_add_find_agent_v2_tables.py

@@ -126,7 +126,6 @@ def upgrade() -> None:
         sa.Column("decision_reason", sa.Text(), nullable=True),
         sa.Column("reject_reason_code", sa.String(64), nullable=True),
         *_timestamps(),
-        sa.ForeignKeyConstraint(["first_search_id"], ["find_agent_v2_search.id"], name="fk_find_agent_v2_candidate_search"),
         sa.PrimaryKeyConstraint("id"),
         sa.UniqueConstraint("run_id", "aweme_id", name="uk_find_agent_v2_candidate_aweme"),
     )

+ 35 - 0
alembic/versions/20260813_14_remove_find_agent_v2_foreign_keys.py

@@ -0,0 +1,35 @@
+"""remove all database foreign keys
+
+Revision ID: 20260813_14
+Revises: 20260812_13
+Create Date: 2026-08-13
+"""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+revision: str = "20260813_14"
+down_revision: str | None = "20260812_13"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    """Drop every FK present in the current database, regardless of generated name."""
+    bind = op.get_bind()
+    inspector = sa.inspect(bind)
+    for table_name in inspector.get_table_names():
+        for constraint in inspector.get_foreign_keys(table_name):
+            name = constraint.get("name")
+            if not name:
+                raise RuntimeError(f"无法删除未命名外键: table={table_name}")
+            op.drop_constraint(name, table_name, type_="foreignkey")
+
+
+def downgrade() -> None:
+    # Forward-only policy: this project must never recreate database FKs.
+    pass

+ 1 - 0
find_agent_v2/AGENT_FLOW.md

@@ -7,6 +7,7 @@
 
 v2 使用“Python 外层业务循环 + 单轮真实 LangGraph + 节点内 LangChain ReAct”的结构。业务数据全部写入
 `find_agent_v2_*` 新表,和旧寻找 Agent 的运行表、工具和日志隔离。
+V2 表不使用任何数据库外键,表间 ID 仅作为应用层逻辑引用。
 
 ```mermaid
 flowchart TD

+ 2 - 0
find_agent_v2/README.md

@@ -18,6 +18,8 @@ Python 外循环(最多 N 个业务轮次)
 每个 ReAct 节点只注册本阶段工具;业务重数据在独立数据库表中,内存 state 只保存轮次、阶段、
 计划和小型快照。
 
+V2 表之间不使用数据库外键;`run_id`、`first_search_id`、`candidate_id` 都是应用层维护的逻辑引用。
+
 节点运行时使用 LangChain `create_agent`,提供模型/工具重试、可选 fallback、Token/费用汇总和
 同阶段受控子 Agent 并发委派。单轮拓扑由真实 `StateGraph` 编译,Obagent 从编译图生成 spec。
 

+ 3 - 6
find_agent_v2/models.py

@@ -5,7 +5,7 @@ from __future__ import annotations
 from datetime import datetime
 from decimal import Decimal
 
-from sqlalchemy import BigInteger, ForeignKey, Index, Integer, Numeric, String, Text, UniqueConstraint, func
+from sqlalchemy import BigInteger, Index, Integer, Numeric, String, Text, UniqueConstraint, func
 from sqlalchemy.dialects.mysql import LONGTEXT
 from sqlalchemy.orm import Mapped, mapped_column
 
@@ -97,11 +97,8 @@ class FindAgentV2Candidate(Base):
 
     id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
     run_id: Mapped[str] = mapped_column(String(64), nullable=False)
-    first_search_id: Mapped[int | None] = mapped_column(
-        BigInteger,
-        ForeignKey("find_agent_v2_search.id", name="fk_find_agent_v2_candidate_search"),
-        nullable=True,
-    )
+    # Logical reference only. find_agent_v2 deliberately uses no database FKs.
+    first_search_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
     aweme_id: Mapped[str] = mapped_column(String(64), nullable=False)
     title: Mapped[str | None] = mapped_column(String(512), nullable=True)
     content_link: Mapped[str | None] = mapped_column(String(1024), nullable=True)

+ 1 - 2
supply_infra/db/models/auth_session.py

@@ -2,7 +2,7 @@ from __future__ import annotations
 
 from datetime import datetime
 
-from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, func
+from sqlalchemy import DateTime, Index, Integer, String, func
 from sqlalchemy.orm import Mapped, mapped_column
 
 from supply_infra.db.base import Base
@@ -26,7 +26,6 @@ class AuthSession(Base):
     )
     user_id: Mapped[int] = mapped_column(
         Integer,
-        ForeignKey("auth_user.id", ondelete="CASCADE"),
         nullable=False,
     )
     expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)

+ 0 - 3
supply_infra/db/models/pipeline_outbox.py

@@ -6,7 +6,6 @@ from typing import Any
 from sqlalchemy import (
     Boolean,
     DateTime,
-    ForeignKey,
     Index,
     JSON,
     String,
@@ -32,12 +31,10 @@ class PipelineOutbox(Base):
     outbox_id: Mapped[str] = mapped_column(String(36), primary_key=True)
     run_id: Mapped[str] = mapped_column(
         String(36),
-        ForeignKey("pipeline_run.run_id", ondelete="CASCADE"),
         nullable=False,
     )
     step_run_id: Mapped[str] = mapped_column(
         String(36),
-        ForeignKey("pipeline_step_run.step_run_id", ondelete="CASCADE"),
         nullable=False,
     )
     effect_type: Mapped[str] = mapped_column(String(64), nullable=False)

+ 0 - 2
supply_infra/db/models/pipeline_step_run.py

@@ -6,7 +6,6 @@ from typing import Any
 from sqlalchemy import (
     Boolean,
     DateTime,
-    ForeignKey,
     Index,
     Integer,
     JSON,
@@ -39,7 +38,6 @@ class PipelineStepRun(Base):
     step_run_id: Mapped[str] = mapped_column(String(36), primary_key=True)
     run_id: Mapped[str] = mapped_column(
         String(36),
-        ForeignKey("pipeline_run.run_id", ondelete="CASCADE"),
         nullable=False,
     )
     step_key: Mapped[str] = mapped_column(String(64), nullable=False)

+ 0 - 6
supply_infra/db/models/video_discovery.py

@@ -5,7 +5,6 @@ from decimal import Decimal
 
 from sqlalchemy import (
     BigInteger,
-    ForeignKey,
     Index,
     Integer,
     Numeric,
@@ -219,11 +218,6 @@ class VideoDiscoveryCandidate(Base):
     run_id: Mapped[str] = mapped_column(String(64), nullable=False, comment="发现运行 run_id")
     search_id: Mapped[int | None] = mapped_column(
         BigInteger,
-        ForeignKey(
-            "video_discovery_search.id",
-            name="fk_video_discovery_candidate_search",
-            ondelete="RESTRICT",
-        ),
         nullable=True,
         comment="直接关联 video_discovery_search.id;历史数据允许为空",
     )

+ 18 - 0
tests/supply_agent/test_find_agent_v2.py

@@ -67,6 +67,13 @@ def test_v2_orm_uses_only_new_table_namespace() -> None:
     assert {"input_tokens", "output_tokens", "total_tokens", "cost_usd"} <= {
         column.name for column in FindAgentV2Run.__table__.columns
     }
+    assert not any(table.foreign_key_constraints for table in (
+        FindAgentV2Run.__table__,
+        FindAgentV2Round.__table__,
+        FindAgentV2Search.__table__,
+        FindAgentV2Candidate.__table__,
+        FindAgentV2Evidence.__table__,
+    ))
 
 
 def test_v2_package_has_no_legacy_business_imports() -> None:
@@ -78,6 +85,17 @@ def test_v2_package_has_no_legacy_business_imports() -> None:
     assert "system_prompt.md" not in source
 
 
+def test_project_orm_defines_no_database_foreign_keys() -> None:
+    import supply_infra.db.models  # noqa: F401
+    from supply_infra.db.base import Base
+
+    assert not {
+        table.name: sorted(fk.name or "<unnamed>" for fk in table.foreign_key_constraints)
+        for table in Base.metadata.tables.values()
+        if table.foreign_key_constraints
+    }
+
+
 def test_v2_owns_age_portrait_normalization() -> None:
     normalized = normalize_age_pair(
         {"年龄": {"50-": {"percentage": "35%", "preference": 120}}},