Skip to content

feat: 双数据库支持 — SQLite + PostgreSQL 并存方案 #487

@hrygo

Description

@hrygo

概述

在保持现有 SQLite 生产级支持的前提下,新增 PostgreSQL 作为可选数据库后端。通过 dbutil.Dialect 抽象层隔离 SQL 方言差异,用户通过一个配置开关(db.driver: postgres)即可切换,无需修改业务代码。

📄 完整规格:docs/specs/Dual-Database-Support-Spec.md

背景

场景 SQLite PostgreSQL
单机部署 ✅ 天然优势 ❌ 需要独立进程
高并发写入(50+ session 同时) ⚠️ WriteMu 串行化 ✅ MVCC 天然并发
多实例共享数据 ❌ 文件锁,单写者 ✅ 原生支持
生产运维(备份/监控) 手动 成熟生态
JSON 查询 json_extract() 有限 jsonb GIN 索引

现状

  • 驱动: 硬编码 modernc.org/sqliteinternal/sqlutil/driver.go
  • 连接: 单例 *sql.DB,5 个消费者共享
  • 迁移: goose v3,硬编码 DialectSQLite3
  • 写序列化: WriteMu 全局互斥锁(SQLite 专有)
  • ORM: 无 — 纯 raw SQL + 嵌入 .sql 文件
  • PG 存量: 零 — pgstore.go 不存在,无任何 PG 驱动导入

已有可复用接口

session.Storecron.Storeeventstore.EventStore + EventTx + TurnQuerier 接口无需改动

缺失抽象

组件 问题
ChatAccessStore 无接口,直接 struct
apiKeyUserStore 无接口,未导出 struct
Admin.Deps.DB 直接暴露 *sql.DB

SQL 方言差异(核心挑战)

特性 SQLite PostgreSQL
占位符 ? $1, $2
标识符引用 `col` "col"
自增主键 AUTOINCREMENT BIGSERIAL
布尔类型 INTEGER (0/1) BOOLEAN
UPSERT ON CONFLICT ... excluded.col ON CONFLICT ... EXCLUDED.col
JSON 操作 json_extract() ->> 操作符
唯一约束错误 字符串匹配 pq.Error.Code == "23505"

架构设计

核心抽象 internal/dbutil/ 包:

type Dialect string  // "sqlite" | "postgres"

func (d Dialect) Rebind(query string) string     // ? → $N 状态机转换
func (d Dialect) Placeholder(n int) string        // ? vs $n
func (d Dialect) QuoteIdent(name string) string   // backtick vs "
func (d Dialect) BoolValue(b bool) any            // 0/1 vs true/false
func (d Dialect) IsUniqueViolation(err error) bool // 错误码匹配

参考项目:Grafana / Gitea / Memos / Focalboard / autobrr / Syncthing

实施计划(9 个 Phase)

Phase 内容 预估
0 提取 ChatAccessStorer + APIKeyUserStorer 接口
1 创建 internal/dbutil/ 包(Dialect + Rebind + DB 包装器)
2 拆分 DBConfig(新增 Driver + PostgresConfig 字段)
3 重构 sqlutil — 移除 SQLite 硬编码
4 参数化 goose 方言 + 创建 9 个 PG 迁移文件
5 实现 PG Store(session/cron/eventstore/chat_access/api_key)
6 DI 容器改造 — 按 Driver 选择 Store 实现
7 测试体系(双驱动 CI 矩阵)
8 文档更新

预估: ~2,430 行新增代码,~295 行修改,约 22 个文件变更。

验收标准

  • db.driver=sqlite 时,行为完全不变(回归测试通过)
  • db.driver=postgres 时,Gateway 启动、创建连接、运行迁移
  • CI 双驱动矩阵通过
  • boolToInt() / isSQLiteUnique() 等 SQLite 专有辅助函数被 Dialect 方法替代
  • 所有 Store 接口的 PG 实现通过测试套件
  • 无 SQLite 专有字符串匹配残留

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Critical: production outage, data loss, securityarchitectureDomain: design patterns, coupling, separation of concernsarea/sessionScope: session manager, state machine, store, poolenhancementFeature: new capabilities or improvements

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions