多章工作流链(docs/design/chain-workflow.md §4/§7)唯一新增 DDL:langgraph 检查点四表(checkpoints/checkpoint_blobs/checkpoint_writes/checkpoint_migrations)。 - 新增迁移 d3e4f5a6b7c8:在迁移内调 langgraph 同步 PostgresSaver.setup() 建表 (DDL 只在 migrations/CI,绝不 app-runtime)。从 database_url 派生 psycopg conn string(去 +asyncpg),与 alembic 的 asyncpg 连接解耦。 - §11 风险落地:setup() 跑 CREATE INDEX CONCURRENTLY,不能在事务块内且会与 alembic 开着的事务死锁 → 用 op.get_context().autocommit_block() 先提交退出 事务块再建表。downgrade 幂等 DROP ... CASCADE。 - env.py 加 include_object:langgraph 四表不在 Base.metadata,过滤掉以免 alembic check 误报漂移。 - jobs 表零迁移(C2 判定:result.awaiting_chapter 表达待裁决,复用现有状态枚举)。 验证:alembic upgrade head 应用、downgrade/re-upgrade 往返、alembic check 无漂移;ruff/format/mypy(packages/db) 全绿。
73 lines
2.2 KiB
Python
73 lines
2.2 KiB
Python
"""Alembic async env(CLAUDE.md:checkpointer.setup 在迁移/CI;此处仅领域表)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
from logging.config import fileConfig
|
||
|
||
import ww_db.models # noqa: F401 注册所有表到 metadata
|
||
from alembic import context
|
||
from sqlalchemy import Connection, pool
|
||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||
from ww_config import get_settings
|
||
from ww_db.base import Base
|
||
|
||
config = context.config
|
||
if config.config_file_name is not None:
|
||
fileConfig(config.config_file_name)
|
||
|
||
# 用 async URL(asyncpg)运行迁移
|
||
config.set_main_option("sqlalchemy.url", get_settings().database_url)
|
||
target_metadata = Base.metadata
|
||
|
||
# langgraph 检查点表由 langgraph 自管 schema(迁移 d3e4f5a6b7c8 调其 setup() 建表),
|
||
# 不在 Base.metadata。autogenerate/check 须忽略它们,否则会误报"待 drop"漂移。
|
||
_LANGGRAPH_TABLES = frozenset(
|
||
{"checkpoints", "checkpoint_blobs", "checkpoint_writes", "checkpoint_migrations"}
|
||
)
|
||
|
||
|
||
def include_object(
|
||
obj: object, name: str | None, type_: str, reflected: bool, compare_to: object
|
||
) -> bool:
|
||
if type_ == "table" and name in _LANGGRAPH_TABLES:
|
||
return False
|
||
return True
|
||
|
||
|
||
def do_run_migrations(connection: Connection) -> None:
|
||
context.configure(
|
||
connection=connection,
|
||
target_metadata=target_metadata,
|
||
include_object=include_object,
|
||
)
|
||
with context.begin_transaction():
|
||
context.run_migrations()
|
||
|
||
|
||
async def run_async_migrations() -> None:
|
||
connectable = async_engine_from_config(
|
||
config.get_section(config.config_ini_section, {}),
|
||
prefix="sqlalchemy.",
|
||
poolclass=pool.NullPool,
|
||
)
|
||
async with connectable.connect() as connection:
|
||
await connection.run_sync(do_run_migrations)
|
||
await connectable.dispose()
|
||
|
||
|
||
def run_migrations_offline() -> None:
|
||
context.configure(
|
||
url=config.get_main_option("sqlalchemy.url"),
|
||
target_metadata=target_metadata,
|
||
literal_binds=True,
|
||
)
|
||
with context.begin_transaction():
|
||
context.run_migrations()
|
||
|
||
|
||
if context.is_offline_mode():
|
||
run_migrations_offline()
|
||
else:
|
||
asyncio.run(run_async_migrations())
|