mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 05:21:57 +00:00
2e7d6103ea
- 配置:database_url、checkpoint_url(启动时校验写法,只认 PostgreSQL)、连接池四项;启动日志写用的是哪种库(主机、库名、用户,不写密码) - 引擎(db/base.make_engine):SQLite 照旧 WAL、外键、忙等;PostgreSQL 用连接池、探活、会话时区 UTC,装上方言调整(db/postgres.py):建表时 String(n) 是不限长的 VARCHAR(SQLite 从不查长度,导过来的存量也可能超长)、ORDER BY 里可空列的 NULL 位置照 SQLite 补写(升序在前、降序在后,窗口函数里同样);Integer 列在 PostgreSQL 上是 BIGINT(列挂到表上时换成 with_variant,绑定参数跟着列按 BIGINT 转换);JSON 列两边都存原文(PostgreSQL 的 json,不用 JSONB:JSONB 重排对象里的键,快照的列序、表的先后读回来就变了);JSON 里的 NaN 写成 null、NUL 去掉(PostgreSQL 不收) - 迁移按方言:已有的列和索引用 inspector 认;PostgreSQL 上补列的类型取模型里这一列的、缺省值取迁移表里的(布尔 0/1 换成 false/true),ADD COLUMN IF NOT EXISTS;去重语句里的布尔按方言写;部分唯一索引补 postgresql_where;init_db 在 PostgreSQL 上拿咨询锁(几个进程同时启动不撞) - 应用库上的 SQLite 专用写法改成按方言编译的表达式(db/portable.py):json_extract 的十几处(口径召回、出具、对账、个人应用、幂等键、访问控制、数据源变化通知、剖析任务、待办超时)换成 json_eq / json_flag / json_text,json_type 换成 json_is_string(两种库同一套带类型的比较);能力登记的 sqlite insert 换成按会话方言的 insert_ignore;能力使用次数的 json_each 按方言分支;附件「没绑记录」的条件不靠 json 的等号;按报错文字认唯一约束的地方换成 unique_violation(按表和列认,两种库的报错都认得) - 断点(engine/checkpoints.py):PostgreSQL 时开 psycopg 连接池、在咨询锁里 setup;runner 只认 BaseCheckpointSaver,读断点仍只走 saver 的公开接口 - 依赖:新 extra postgres(asyncpg、psycopg[binary] 3、psycopg-pool、langgraph-checkpoint-postgres 3.1.2);langgraph-checkpoint 卡在 <4.3(断点格式就在这个包里,postgres 包新版要 4.3,不动已装的 4.2.0);锁文件补上新包 - 运行收尾:记 action.unknown、安排核实做完才把运行摘掉(以前先摘掉,等「不在跑了」再读事件的一方在库慢时读不到这一条) - 测试:AGENTLAB_TEST_DATABASE_URL 时跑在 PostgreSQL 上(每个 xdist 进程建自己的库、按事件循环分的连接池 tests/pgpool.py),不设时盖掉环境里的数据库地址照旧用 SQLite;scripts/test-postgres.sh 起临时的 postgres:16 容器跑全量;sqlite_only 标记;另开连接改库的几个竞态测试、触发器测试改成两种库都能跑(tests/appdb.py);新增 test_db_portable 守着方言调整、JSON 键序、超长字符串、大整数、NULL 排序、时区、PostgreSQL 老库迁移
77 lines
4.1 KiB
Python
77 lines
4.1 KiB
Python
"""应用库是 PostgreSQL 时,让它在 SQLite 上写成的那些假设下照样成立。
|
||
|
||
只装在应用库那个引擎的方言实例上(db/base.make_engine),业务数据源的 PostgreSQL 引擎不受影响。
|
||
|
||
**建表的列类型。** 应用是在 SQLite 上写成的:SQLite 不查 VARCHAR 的长度,INTEGER 是 64 位。PostgreSQL 上
|
||
VARCHAR(n) 超长直接报错、INTEGER 只有 32 位。模型里的 String(n) 是写给人看的「大致多长」,从来没有被库约束过:
|
||
逐列核对只核得到测试走到的值,漏一处就是线上的写入失败,从 SQLite 导过来的存量数据里也可能已经有超长的。所以
|
||
应用库上 String(n) 建成不限长的 VARCHAR(PostgreSQL 里 varchar、varchar(n)、text 存储和性能一样,限长只多一次
|
||
检查)。CAST(x AS VARCHAR(n)) 这种查询里的写法不动——那是截断的意思。Integer 列在 PostgreSQL 上是 BIGINT:
|
||
那是在模型这一层换的(db/base._portable_integer),绑定参数的类型转换跟着列走,大数不在半路溢出。
|
||
|
||
**ORDER BY 里 NULL 的位置。** SQLite 升序 NULL 在前、降序 NULL 在后,PostgreSQL 正好相反。应用里两百多处排序,
|
||
可空的列(开始时间、到期时间、发布时间……)排出来的先后要和 SQLite 一致:可空列上没写 nulls_first / nulls_last
|
||
的 ASC 补 NULLS FIRST、DESC 补 NULLS LAST,ORDER BY 里直接写列名(隐含 ASC)的同样补上。不可空的列不动——
|
||
补了会让索引用不上,又没有 NULL 可排。窗口函数 OVER (ORDER BY …) 里同样补。原生 text() 里写的 ORDER BY 不经过
|
||
这里,要自己写明(db/base 的去重语句里就写了)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from sqlalchemy import Column
|
||
from sqlalchemy.dialects.postgresql.asyncpg import PGCompiler_asyncpg
|
||
from sqlalchemy.dialects.postgresql.base import PGTypeCompiler
|
||
from sqlalchemy.sql import operators
|
||
from sqlalchemy.sql.elements import ClauseList, UnaryExpression
|
||
|
||
#: 外层已经写了 nulls_first / nulls_last:里面那一层 ASC / DESC 不再补
|
||
_EXPLICIT = "_agentlab_explicit_nulls"
|
||
|
||
|
||
def _nullable(element: Any) -> bool:
|
||
return isinstance(element, Column) and bool(element.nullable) and not element.primary_key
|
||
|
||
|
||
class AppTypeCompiler(PGTypeCompiler):
|
||
def visit_string(self, type_: Any, **kw: Any) -> str:
|
||
if type_.length and isinstance(kw.get("type_expression"), Column):
|
||
return "VARCHAR"
|
||
return super().visit_string(type_, **kw)
|
||
|
||
|
||
def _ordered(clauses: Any) -> list[Any]:
|
||
"""ORDER BY 里直接写的可空列补成 ASC(visit_unary 再给它补 NULLS FIRST)。"""
|
||
return [c.asc() if _nullable(c) else c for c in clauses]
|
||
|
||
|
||
class AppCompiler(PGCompiler_asyncpg):
|
||
def order_by_clause(self, select: Any, **kw: Any) -> str:
|
||
order_by = self._generate_delimited_list(_ordered(select._order_by_clauses), ", ", **kw)
|
||
return " ORDER BY " + order_by if order_by else ""
|
||
|
||
def visit_over(self, over: Any, **kw: Any) -> str:
|
||
clauses = list(over.order_by) if over.order_by is not None else []
|
||
if any(_nullable(c) for c in clauses):
|
||
# 在副本上换:语句对象可能被缓存、被别处共用,不能改原件
|
||
over = over._clone()
|
||
over.order_by = ClauseList(*_ordered(clauses))
|
||
return super().visit_over(over, **kw)
|
||
|
||
def visit_unary(self, unary: UnaryExpression, **kw: Any) -> str:
|
||
modifier = unary.modifier
|
||
if modifier in (operators.nulls_first_op, operators.nulls_last_op):
|
||
return super().visit_unary(unary, **{**kw, _EXPLICIT: True})
|
||
explicit = kw.pop(_EXPLICIT, False)
|
||
text = super().visit_unary(unary, **kw)
|
||
if not explicit and modifier in (operators.asc_op, operators.desc_op) and _nullable(unary.element):
|
||
text += " NULLS FIRST" if modifier is operators.asc_op else " NULLS LAST"
|
||
return text
|
||
|
||
|
||
def install(dialect: Any) -> None:
|
||
"""装到应用库引擎的方言实例上。"""
|
||
dialect.statement_compiler = AppCompiler
|
||
dialect.type_compiler_instance = dialect.type_compiler = AppTypeCompiler(dialect)
|