Skip to content

feat: 数据库层方言可插拔(SQLite → 高斯/MySQL/达梦易扩展) - #39

Open
tianling536 wants to merge 6 commits into
OpenBMB:mainfrom
tianling536:feat/multi-db-clean
Open

feat: 数据库层方言可插拔(SQLite → 高斯/MySQL/达梦易扩展)#39
tianling536 wants to merge 6 commits into
OpenBMB:mainfrom
tianling536:feat/multi-db-clean

Conversation

@tianling536

Copy link
Copy Markdown
Contributor

概述

将数据库层从"SQLite 实现"升级为方言可插拔:默认走 ORM/标准 SQL(所有数据库通用),无法通用的少数点收口到方言提供者接口。新增一种数据库 = 注册一个小适配器(锁/日期/能力声明),业务代码零改动。高斯(openGauss)经 PostgreSQL 协议直接可用,MySQL/达梦按 docstring 中的扩展方式接入。

设计

新模块 backend/app/db/dialect.py

  • DatabaseDialect 协议:engine_kwargs(引擎/连接池配置)、day_bucket(自然日分桶)、JSON 配置读改写、acquire/release_advisory_locksupports_partial_index 能力声明;
  • 注册表:sqlite → SQLiteDialectpostgresql → PostgresDialect(高斯直接复用,pg_try_advisory_lock)、未知名 → GenericDialect(全部默认实现);
  • 默认实现即通用实现(ORM 读改写、cast(..., Date)、数据目录文件锁),因此即使无适配器的新数据库也能先跑起来,适配器只补特性。

改造点

  1. 引擎创建get_dialect(backend).engine_kwargs(url)(SQLite 行为逐字不变);
  2. 收编 4 处方言 SQL:
    • api/channels.py 配置补丁(SQLite JSON1)→ ORM 读-改-写;
    • adapters/wechat.py::_patch_runtime_config(JSON1+CAS)→ ORM 读-改-写 + config_revision 乐观锁 CAS,并发语义三层保住(进程锁+revision CAS+Python 校验),微信全链路测试零回归并加 2 个 CAS 补强测试;
    • api/knowledge.pyCAST AS BLOB → 方言分支(SQLite 保留,其它走 ORM);
    • 投递日志日分桶 → 方言助手(PG 支持 app_timezone 配置,空值回退服务器本地偏移对齐 SQLite localtime 语义);
  3. 两个索引并入 models.py(uq_sessions_agent_channel_extconvuq_model_configs_tenant_default 部分索引 sqlite_where="is_default = 1"/postgresql_where="is_default"),新库 create_all 即完整 schema,与 SQLite 迁移里的幂等 CREATE INDEX IF NOT EXISTS 共存;
  4. 渠道连接器方言化:connector 进程锁走方言提供器(PG 用 advisory lock+常驻会话持锁,SQLite 保持文件锁,方言锁内置 fork 防护);飞书链路传完整 SQLAlchemy URL,放开"仅文件 SQLite"守卫(仅拒绝内存库);
  5. 依赖:+ psycopg[binary](GaussDB 标准 PG 协议);MySQL/DM 不预装,docstring 注明扩展方式。

验证

  • SQLite 零回归:全量 1023 passed / ruff 零告警;
  • 真 PostgreSQL 16(Docker)实证:create_all+索引、应用启动(api/all 角色)、connector advisory lock、登录/种子/查询、渠道绑定与凭证(JSON 补丁)、日分桶、知识库,全部通过;
  • openGauss:受限于本机 Apple Silicon(官方镜像无法启动)与 CI runner 不可用,未能实测真机;已附 .github/workflows/gaussdb-smoke.yml(在 x86 CI 一键起 openGauss 容器跑全套方言冒烟),作为后续验证跟进项——openGauss 走 PG 协议,代码内对 advisory lock/JSON 函数均有回退路径,风险集中且可控。

后续(登记项,不在本 PR)

  • openGauss 真机/CI 验证;
  • MySQLDialect / DMDialect 适配器实现(骨架与能力说明已在 dialect.py docstring);
  • 存量 SQLite 数据向 GaussDB 搬迁的一次性导数(非代码改造)。

Comment on lines +8 to +109
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Start openGauss
run: |
docker run -d --name opengauss \
-e GS_PASSWORD='Staffdeck@123' \
-p 5432:5432 \
enmotech/opengauss:5.0.0
for i in $(seq 1 120); do
if docker exec opengauss su - omm -c "gsql -d postgres -c 'select 1'" >/dev/null 2>&1; then
echo "openGauss ready after ${i}s"
break
fi
sleep 2
if [ "$i" -eq 120 ]; then
echo "openGauss failed to become ready"
docker logs opengauss
exit 1
fi
done
docker exec opengauss su - omm -c "gsql -d postgres -c 'select version()'"

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install backend deps
run: |
python -m venv .venv
.venv/bin/pip install -q -e backend[dev] 2>/dev/null || (cd backend && ../.venv/bin/pip install -q -e . && ../.venv/bin/pip install -q pytest pytest-asyncio ruff httpx)

- name: create_all + dialect smoke on openGauss
env:
DATABASE_URL: postgresql+psycopg://postgres:Staffdeck%40123@127.0.0.1:5432/postgres
STAFFDECK_ROLE: all
working-directory: backend
run: |
set -e
../.venv/bin/python - <<'PY'
from app.db.database import init_db
init_db()
print("create_all on openGauss: OK")
from sqlalchemy import inspect
from app.db import engine
idx = {i["name"] for t in ("sessions", "model_configs") for i in inspect(engine).get_indexes(t)}
assert "uq_sessions_agent_channel_extconv" in idx, "missing session index"
assert "uq_model_configs_tenant_default" in idx, "missing default-model partial index"
print("indexes: OK")
from app.db.dialect import get_dialect
from sqlmodel import Session
d = get_dialect(engine.url.get_backend_name())
print("dialect:", d.name)
assert d.acquire_advisory_lock("staffdeck-connector")
print("advisory lock acquire: OK")
d.release_advisory_lock("staffdeck-connector")
print("advisory lock release: OK")
with Session(engine) as db:
from app.db.models import User
db.exec(__import__("sqlmodel").select(User)).all()
print("basic query: OK")
PY

- name: Boot app and API smoke on openGauss
env:
DATABASE_URL: postgresql+psycopg://postgres:Staffdeck%40123@127.0.0.1:5432/postgres
STAFFDECK_ROLE: all
ULTRARAG_PORT: 5199
working-directory: backend
run: |
set -e
../.venv/bin/python -m uvicorn app.main:app --host 127.0.0.1 --port 5199 &
APP_PID=$!
for i in $(seq 1 60); do
if curl -sf http://127.0.0.1:5199/api/health >/dev/null 2>&1; then echo "healthy after ${i}s"; break; fi
sleep 1
if [ "$i" -eq 60 ]; then echo "app failed to start"; kill $APP_PID; exit 1; fi
done
curl -sf http://127.0.0.1:5199/api/health
TOKEN=$(curl -sf -X POST http://127.0.0.1:5199/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"tenant_id":"tenant_demo","username":"admin","password":"admin"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
echo "login: OK"
AGENTS=$(curl -sf "http://127.0.0.1:5199/api/chat/agents?tenant_id=tenant_demo" -H "Authorization: Bearer $TOKEN")
echo "agents: $(echo "$AGENTS" | python3 -c 'import sys,json;print(len(json.load(sys.stdin)))')"
BID=$(curl -sf -X POST http://127.0.0.1:5199/api/enterprise/channels \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"tenant_id":"tenant_demo","agent_id":"agent_30b8f623c6fe445b","channel":"wecom"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
echo "binding: $BID"
curl -sf -X POST "http://127.0.0.1:5199/api/enterprise/channels/$BID/wecom/credentials" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"tenant_id":"tenant_demo","bot_id":"smoke_bot","secret":"smoke_secret","corp_id":"smoke_corp"}' -o /dev/null
echo "credentials(JSON config patch): OK"
curl -sf "http://127.0.0.1:5199/api/enterprise/channels/$BID/deliveries/days?tenant_id=tenant_demo" \
-H "Authorization: Bearer $TOKEN" | python3 -c 'import sys,json;print("day-bucket endpoint:", json.load(sys.stdin)["total_days"], "days")'
curl -sf "http://127.0.0.1:5199/api/enterprise/knowledge-bases?tenant_id=tenant_demo" \
-H "Authorization: Bearer $TOKEN" | python3 -c 'import sys,json;print("knowledge-bases:", len(json.load(sys.stdin)))'
kill $APP_PID || true
@hm1229
hm1229 requested a review from dxyhymn July 27, 2026 03:09
田领 added 6 commits July 27, 2026 23:19
- 新增 app/db/dialect.py 方言提供者(协议+注册表+
  SQLite/Postgres/Generic 实现),引擎创建走方言 kwargs
- 收编 4 处生产方言 SQL:渠道配置补丁与微信运行时补丁
  改 ORM 读改写+revision CAS,知识库 BLOB 查询方言分支,
  日分桶方言助手(PG 支持 app_timezone)
- 两个索引并入 models.py,新库 create_all 即完整 schema
- 加 psycopg[binary] 驱动与 PG 冒烟脚本

全量 1006 passed,ruff 零告警
- connector 进程锁走方言提供器:PG 用 advisory lock
  (常驻会话持锁),SQLite 等保持数据目录文件锁
- 方言文件锁内置 fork 防护(pid 判别,子进程不误解父锁)
- 飞书链路传完整 SQLAlchemy URL 替代 DB 文件路径,
  放开文件 SQLite 守卫(仅拒绝内存库)

全量 1013 passed,ruff 零告警
main 合入的渠道加固在企微重连清理 disconnect_alerted_at 时用了 SQLite 专有
json_remove;改为读-改-写 + dialect.json_config_remove,与微信 _patch_runtime_config
同一模式,PG/openGauss 可用
@tianling536
tianling536 force-pushed the feat/multi-db-clean branch from dd9add0 to 7945867 Compare July 27, 2026 15:27
@tianling536

Copy link
Copy Markdown
Contributor Author

已 rebase 到最新 main(cd64f92,含渠道加固合并),冲突已解决,当前 MERGEABLE。

本次 rebase 要点:

  1. 冲突仅 1 处(test_channel_wechat.py):main 的 client_id 分片测试与本分支的 revision CAS 测试段落位置相邻,两边都保留。
  2. 收编 main 新增的一处 SQLite 专有 SQL(新 commit 7945867):渠道加固在企微重连清理 disconnect_alerted_at 时使用了 json_remove 裸 SQL,PG/openGauss 下会报错;已改为与微信 _patch_runtime_config 相同的 ORM 读-改-写 + dialect.json_config_remove 模式。已全仓扫描确认这是 main 新增代码中唯一未方言化的原生 SQL。
  3. 真实 PostgreSQL 16 冒烟重跑通过(create_all / 方言路径 / 应用启动 / /api/health,schema 重置后)。

验证:后端全量 1201 passed、ruff 通过。

另注:test_model_configs_api.py::test_verification_runs_bounded_text_stream_and_json_probes 失败为 main 预存在问题(纯净 main 同样复现,bounded probe 期望 max_tokens=32 实际 8192),与本 PR 无关。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants